Files

237 lines
12 KiB
JavaScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
'use strict';
const fs = require('fs');
const path = require('path');
function parseArgs(argv) {
const args = { input: null, json: false, markdown: false };
for (let i = 2; i < argv.length; i++) {
const a = argv[i];
if (a === '--input' || a === '-i') args.input = argv[++i];
else if (a === '--json') args.json = true;
else if (a === '--markdown') args.markdown = true;
else if (a === '--help' || a === '-h') args.help = true;
else throw new Error(`未知参数:${a}`);
}
if (!args.json && !args.markdown) args.markdown = true;
return args;
}
function usage() {
return `用法:\n node scripts/check_intake.js --input task.md --markdown\n node scripts/check_intake.js --input task.md --json\n\n不提供 --input 时会从标准输入读取内容。`;
}
function readInput(input) {
if (input) return fs.readFileSync(input, 'utf8');
return fs.readFileSync(0, 'utf8');
}
function cleanValue(v) {
if (!v) return '';
return v.trim().replace(/^`+|`+$/g, '').trim();
}
function isPlaceholder(v) {
const s = cleanValue(v).toLowerCase();
return !s || ['无', '暂无', '未知', '不确定', 'todo', 'tbd', 'n/a', 'na', 'none', 'null', '-', ':', ':'].includes(s) || /^[.。…]+$/.test(s);
}
function lineValue(text, labels) {
const lines = text.split(/\r?\n/);
for (const line of lines) {
const trimmed = line.trim();
for (const label of labels) {
const pattern = new RegExp(`^(?:[-*+]\\s*)?(?:\\[[ xX]\\]\\s*)?${label}\\s*[::]\\s*(.+?)\\s*$`, 'i');
const m = trimmed.match(pattern);
if (m && !isPlaceholder(m[1])) return cleanValue(m[1]);
}
}
return '';
}
function firstUrlNear(text, labels) {
const direct = lineValue(text, labels);
if (direct) {
const m = direct.match(/https?:\/\/[^\s`'"<>]+/i);
if (m) return m[0];
if (/^[\w.-]+\.[a-z]{2,}/i.test(direct)) return direct;
}
for (const label of labels) {
const re = new RegExp(`${label}[^\n\r]*(https?:\\/\\/[^\\s\`'"<>]+)`, 'i');
const m = text.match(re);
if (m) return m[1];
}
const domain = text.match(/(?:目标|网站|站点|页面|target)[^\n\r]{0,40}?([A-Za-z0-9.-]+\.[A-Za-z]{2,}(?:\/[^\s`'"<>]*)?)/i);
if (domain) return domain[1];
return '';
}
function detectMethod(text) {
const direct = lineValue(text, ['请求方法', 'Method', 'HTTP Method', 'Request Method']);
const src = direct || text;
const m = src.match(/\b(GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD)\b/i);
return m ? m[1].toUpperCase() : '';
}
function detectLocation(text) {
const direct = lineValue(text, ['参数出现位置', '参数位置', '加密参数位置', 'Parameter Location', 'Location']);
const src = direct || text;
const locations = ['Query', 'Header', 'Body', 'Cookie'];
for (const loc of locations) {
if (new RegExp(`\\b${loc}\\b`, 'i').test(src)) return loc;
}
const zh = [ ['query', /查询|URL参数|URL 参数/i], ['header', /请求头|Header/i], ['body', /请求体|Body|表单|JSON/i], ['cookie', /Cookie/i] ];
for (const [name, re] of zh) if (re.test(src)) return name[0].toUpperCase() + name.slice(1);
return '';
}
function detectParam(text) {
const direct = lineValue(text, ['目标加密参数名', '目标加密参数', '加密参数名', '加密参数', '参数名', 'Encrypted Parameter', 'Parameter']);
if (direct) {
const m = direct.match(/[A-Za-z_$][\w$-]{0,80}/);
if (m) return m[0];
return direct;
}
const common = ['__zse_ck', 'zse_ck', 'a_bogus', 'h5st', 'x-s', 'x-t', 'x-sign', 'sign', 'signature', 'token'];
for (const p of common) {
if (new RegExp(`(^|[^\\w-])${p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\w-]|$)`, 'i').test(text)) return p;
}
return '';
}
function hasRequestSample(text) {
if (/copy\s+as\s+curl/i.test(text) && /(已提供|提供|粘贴|curl)/i.test(text)) return true;
if (/\bcurl\b\s+['"]?https?:\/\//i.test(text)) return true;
if (/\bcurl\b[\s\S]{0,300}https?:\/\//i.test(text)) return true;
if (/^(GET|POST|PUT|DELETE|PATCH)\s+https?:\/\//im.test(text)) return true;
const v = lineValue(text, ['成功请求样本', '请求样本', 'Request Sample', 'cURL', 'curl']);
return !!v && !/未提供|没有|无/.test(v);
}
function hasResponseSample(text) {
if (/响应样本[::]\s*(已提供|有|是)/i.test(text)) return true;
if (/```json[\s\S]*?```/i.test(text) && /响应|response/i.test(text)) return true;
return false;
}
function hasKnownJs(text) {
if (/https?:\/\/[^\s`'"<>]+\.js(?:\?[^\s`'"<>]*)?/i.test(text)) return true;
if (/JS\s*文件[::]\s*(已提供|有|是)/i.test(text)) return true;
return false;
}
function detectAcquisitionMode(text) {
const direct = lineValue(text, ['取证模式', '浏览器取证模式', '浏览器模式', '取证工具', '浏览器工具', 'Ruyi 工具选择', 'Ruyi工具选择', 'Acquisition Mode']);
const checked = text.match(/^\s*[-*+]\s*\[[xX]\]\s*(ruyiPage\s*\+\s*RuyiTrace|仅\s*ruyiPage|只用\s*ruyiPage|Camoufox\s*\+\s*camoufox-reverse-mcp|仅\s*Camoufox|只用\s*Camoufox|CloakBrowser|用户手动取证|手动取证|AI\s*自行决定)[^\n\r]*/im);
const src = direct || (checked ? checked[1] : '');
if (/ruyiPage\s*\+\s*RuyiTrace|ruyiPage.*RuyiTrace|如意\s*Trace/i.test(src)) return 'ruyiPage + RuyiTrace';
if (/仅\s*ruyiPage|只用\s*ruyiPage|ruyiPage/i.test(src)) return '仅 ruyiPage';
if (/Camoufox\s*\+\s*camoufox-reverse-mcp|Camoufox.*MCP|camoufox-reverse-mcp/i.test(src)) return 'Camoufox + camoufox-reverse-mcp';
if (/仅\s*Camoufox|只用\s*Camoufox|Camoufox/i.test(src)) return '仅 Camoufox';
if (/CloakBrowser/i.test(src)) return 'CloakBrowser';
if (/用户手动取证|手动取证|离线材料|手动提供|手动浏览器/i.test(src)) return '用户手动取证';
if (/AI\s*自行决定|自行决定|你来决定|自动选择/i.test(src)) return 'AI 自行决定';
if (/(选择|使用|采用|取证模式是|用)\s*(ruyiPage\s*\+\s*RuyiTrace|ruyiPage.*RuyiTrace)/i.test(text)) return 'ruyiPage + RuyiTrace';
if (/(选择|使用|采用|取证模式是|用)\s*(仅\s*)?ruyiPage/i.test(text)) return '仅 ruyiPage';
if (/(选择|使用|采用|取证模式是|用).*(Camoufox\s*\+\s*camoufox-reverse-mcp|Camoufox.*MCP|camoufox-reverse-mcp)/i.test(text)) return 'Camoufox + camoufox-reverse-mcp';
if (/(选择|使用|采用|取证模式是|用).*(仅\s*)?Camoufox/i.test(text)) return '仅 Camoufox';
if (/(选择|使用|采用|取证模式是|用)\s*CloakBrowser/i.test(text)) return 'CloakBrowser';
if (/(选择|使用|采用|取证模式是|用).*(用户手动取证|手动取证|离线材料|手动提供)/i.test(text)) return '用户手动取证';
if (/(选择|使用|采用|取证模式是|用).*(AI\s*自行决定|自行决定|你来决定|自动选择)/i.test(text)) return 'AI 自行决定';
return '';
}
function detectTlsClientStrategy(text) {
const direct = lineValue(text, ['最终请求 TLS 指纹兼容客户端', 'TLS 指纹兼容客户端', 'TLS 请求客户端', '最终请求客户端', '请求客户端', 'TLS Strategy', 'TLS Client']);
const src = direct || text;
if (/Node\.?js.*CycleTLS|CycleTLS|cycleTls|cycletls/i.test(src)) return 'Node.js CycleTLS';
if (/Node\.?js.*impers|\bimpers\b/i.test(src)) return 'Node.js impers';
if (/Node\.?js.*curl-cffi|curl-cffi-node|curl-cffi/i.test(src)) return 'Node.js curl-cffi';
if (/Python.*curl_cffi|curl_cffi/i.test(src)) return 'Python curl_cffi';
if (/Python.*cffi_curl|cffi_curl/i.test(src)) return 'Python cffi_curl';
if (/Python.*cyCronet|cyCronet|cycronet/i.test(src)) return 'Python cyCronet';
if (/不发真实请求|不发送真实请求|只输出本地\s*(sign|参数)|仅输出本地\s*(sign|参数)|只做本地验证/i.test(src)) return '不发真实请求,只输出本地 sign / 参数';
return '';
}
function analyze(text) {
const fields = {
targetSiteUrl: firstUrlNear(text, ['目标网站 URL', '目标网站', '目标站点', '目标页面', '网站 URL', 'Target Website', 'Target Site', 'Page URL']),
apiUrl: firstUrlNear(text, ['目标 API URL', '目标 API', '目标接口 API', '目标接口', '接口 API', '请求 URL', 'API URL', 'Request URL', 'Target API']),
method: detectMethod(text),
encryptedParam: detectParam(text),
paramLocation: detectLocation(text),
requestSample: hasRequestSample(text),
responseSample: hasResponseSample(text),
knownJsAssets: hasKnownJs(text),
loginRequired: /需要登录[::]?\s*(是|true|yes)|登录后|已登录|login required/i.test(text),
authScopeMentioned: /授权|测试权限|permission|authorized/i.test(text),
acquisitionMode: detectAcquisitionMode(text),
tlsClientStrategy: detectTlsClientStrategy(text),
};
// If API URL duplicated from target site and a more specific URL exists in curl, use the curl URL as apiUrl.
const curlUrl = (text.match(/\bcurl\b[\s\S]{0,500}?(https?:\/\/[^\s`'"<>]+)/i) || [])[1];
if (curlUrl && (!fields.apiUrl || fields.apiUrl === fields.targetSiteUrl)) fields.apiUrl = curlUrl;
if (fields.apiUrl === fields.targetSiteUrl && !/(目标接口|接口\s*(API|URL)|API\s*(URL|地址)?|请求\s*URL|Request URL|Target API)/i.test(text)) fields.apiUrl = '';
const required = [
['targetSiteUrl', '目标网站/页面 URL'],
['apiUrl', '目标接口 API URL'],
['method', '请求方法'],
['encryptedParam', '目标加密参数名'],
['paramLocation', '参数出现位置 Query/Header/Body/Cookie'],
['requestSample', '成功请求样本(优先 Copy as cURL 或 HAR)'],
['acquisitionMode', '取证模式(ruyiPage + RuyiTrace / 仅 ruyiPage / Camoufox + camoufox-reverse-mcp / 仅 Camoufox / CloakBrowser / 用户手动取证 / AI 自行决定)'],
['tlsClientStrategy', '最终请求 TLS 指纹兼容客户端(Node.js CycleTLS / Node.js impers / Node.js curl-cffi / Python curl_cffi / Python cffi_curl / Python cyCronet / 不发真实请求)'],
];
const missing = required.filter(([key]) => !fields[key]).map(([, label]) => label);
const complete = missing.length === 0;
return { complete, missing, fields };
}
function renderMarkdown(result) {
const f = result.fields;
const yesNo = v => v ? '是' : '否';
const lines = [];
lines.push('# Web JS 补环境信息完整性检查结果');
lines.push('');
lines.push(`- 是否满足开始前置分析的最低要求:${result.complete ? '是' : '否'}`);
lines.push(`- 目标网站/页面 URL:${f.targetSiteUrl || '未提供'}`);
lines.push(`- 目标接口 API URL:${f.apiUrl || '未提供'}`);
lines.push(`- 请求方法:${f.method || '未提供'}`);
lines.push(`- 目标加密参数:${f.encryptedParam || '未提供'}`);
lines.push(`- 参数位置:${f.paramLocation || '未提供'}`);
lines.push(`- 已提供成功请求样本:${yesNo(f.requestSample)}`);
lines.push(`- 已提供成功响应样本:${yesNo(f.responseSample)}`);
lines.push(`- 已知 JS 文件:${yesNo(f.knownJsAssets)}`);
lines.push(`- 是否提到登录:${yesNo(f.loginRequired)}`);
lines.push(`- 取证模式:${f.acquisitionMode || '未提供'}`);
lines.push(`- 最终请求 TLS 指纹兼容客户端:${f.tlsClientStrategy || '未提供'}`);
lines.push('');
if (!result.complete) {
lines.push('## 当前缺少的必要信息');
for (const item of result.missing) lines.push(`- [ ] ${item}`);
lines.push('');
lines.push('请先补充以上必要信息;补齐前不要进入逆向分析或补环境代码阶段。');
} else {
lines.push('## 下一步');
lines.push('请先把以上关键信息、取证模式和最终请求 TLS 指纹兼容客户端整理给用户确认;用户确认后再校验请求样本、加密参数和 JS 文件可获取性。后续所有浏览器取证动作必须沿用用户确认的取证模式;最终真实请求只能使用用户确认的 Node.js / Python 请求客户端。');
}
return lines.join('\n') + '\n';
}
try {
const args = parseArgs(process.argv);
if (args.help) { console.log(usage()); process.exit(0); }
const text = readInput(args.input);
const result = analyze(text);
if (args.json) console.log(JSON.stringify(result, null, 2));
if (args.markdown) process.stdout.write(renderMarkdown(result));
} catch (err) {
console.error(err.message || String(err));
console.error(usage());
process.exit(1);
}