Files
reverse-skills-max/vendor-new/js-reverse-automation--skill/js-reverse-automation/scripts/ast_candidate_analyzer.js

135 lines
4.8 KiB
JavaScript

#!/usr/bin/env node
"use strict";
/**
* Static AST candidate analyzer for JavaScript bundles.
*
* Parses JS files using Babel (with fallback regex parser) to find function
* declarations matching crypto-related keywords.
*
* Usage:
* node scripts/ast_candidate_analyzer.js --input bundle.js --output artifacts/static_candidates.json
*/
const fs = require("fs");
const path = require("path");
let babelParser = null;
try { babelParser = require("@babel/parser"); } catch (_) {}
function args() {
const out = {};
for (let i = 2; i < process.argv.length; i += 2)
out[process.argv[i].replace(/^--/, "")] = process.argv[i + 1];
if (!out.input || !out.output)
throw new Error("Usage: ast_candidate_analyzer.js --input bundle.js --output static_candidates.json");
return out;
}
const KEYWORDS = /encrypt|decrypt|cipher|sign|hmac|digest|hash|rsa|aes|des|sm2|sm3|sm4|md5|sha|base64|crypto/i;
function sourceSnippet(code, start, end) {
return code.slice(start || 0, Math.min(end || code.length, (start || 0) + 1200));
}
function locationFromOffset(code, offset) {
const before = code.slice(0, offset);
const lines = before.split("\n");
return { line: lines.length, column: lines[lines.length - 1].length };
}
function signalsFor(name, snippet) {
const signals = [];
if (KEYWORDS.test(name)) signals.push("name_keyword");
if (KEYWORDS.test(snippet)) signals.push("source_keyword");
if (/crypto\.subtle|CryptoJS|JSEncrypt|sm-crypto|jsrsasign|forge/i.test(snippet))
signals.push("known_library");
if (/publicKey|privateKey|BEGIN PUBLIC KEY|BEGIN RSA/i.test(snippet))
signals.push("key_material");
return signals;
}
function addCandidate(map, code, name, start, end, mode) {
const snippet = sourceSnippet(code, start, end);
const signals = signalsFor(name, snippet);
if (!signals.length) return;
const loc = locationFromOffset(code, start);
const key = `${name}:${loc.line}:${loc.column}`;
map.set(key, {
id: `static:${key}`, name, path: name, type: "function",
location: loc, signals, source_snippet: snippet, parser_mode: mode
});
}
function walk(node, parent, visit) {
if (!node || typeof node !== "object") return;
if (typeof node.type === "string") visit(node, parent);
for (const [key, value] of Object.entries(node)) {
if (["loc", "start", "end", "extra", "tokens", "comments"].includes(key)) continue;
if (Array.isArray(value)) value.forEach(child => walk(child, node, visit));
else if (value && typeof value === "object") walk(value, node, visit);
}
}
function nameOf(node, parent) {
if (node.id && node.id.name) return node.id.name;
if (parent && parent.type === "VariableDeclarator" && parent.id && parent.id.name)
return parent.id.name;
if (parent && /ObjectProperty|ObjectMethod|ClassMethod/.test(parent.type || ""))
return parent.key && (parent.key.name || parent.key.value) || "anonymous";
return "anonymous";
}
function analyzeWithBabel(code, map) {
const ast = babelParser.parse(code, {
sourceType: "unambiguous",
errorRecovery: true,
plugins: ["jsx", "typescript", "classProperties", "optionalChaining", "dynamicImport", "decorators-legacy"]
});
walk(ast, null, (node, parent) => {
if (!["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression",
"ObjectMethod", "ClassMethod"].includes(node.type)) return;
addCandidate(map, code, nameOf(node, parent), node.start, node.end, "babel");
});
}
function analyzeFallback(code, map) {
const patterns = [
/function\s+([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*\{/g,
/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?(?:function\s*)?\([^)]*\)\s*=>?\s*\{/g,
/([A-Za-z_$][\w$]*)\s*:\s*(?:async\s*)?function\s*\([^)]*\)\s*\{/g
];
for (const pattern of patterns) {
let match;
while ((match = pattern.exec(code)))
addCandidate(map, code, match[1], match.index, Math.min(code.length, match.index + 3000), "fallback-regex");
}
}
function main() {
const opt = args();
const code = fs.readFileSync(opt.input, "utf8");
const map = new Map();
if (babelParser) analyzeWithBabel(code, map);
else analyzeFallback(code, map);
const result = {
input: path.resolve(opt.input),
parser_mode: babelParser ? "babel" : "fallback-regex",
candidates: Array.from(map.values())
};
fs.mkdirSync(path.dirname(opt.output), { recursive: true });
fs.writeFileSync(opt.output, JSON.stringify(result, null, 2) + "\n");
console.log(JSON.stringify({
status: "ok",
parser_mode: result.parser_mode,
candidates: result.candidates.length,
output: opt.output
}));
}
try { main(); }
catch (error) { console.error(String(error.stack || error)); process.exit(2); }