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

131 lines
6.4 KiB
JavaScript

#!/usr/bin/env node
"use strict";
/**
* Conservative source-level property instrumentation.
*
* This tool intentionally rewrites only explicitly selected member reads such
* as navigator.webdriver, document.cookie,
* document.getElementById("username").value, or
* document["getElementById"]("username")["value"]. It skips strings,
* comments, assignments, and calls so it can be used as a low-risk first tap
* before broader AST instrumentation is introduced.
*
* Usage:
* node scripts/source_instrumentor.js --input bundle.js --output bundle.tap.js \
* --properties webdriver,userAgent,cookie --objects navigator,document
*/
const fs = require("fs");
const path = require("path");
function parseArgs() {
const result = {};
for (let i = 2; i < process.argv.length; i += 2) {
const key = process.argv[i];
if (!key.startsWith("--")) throw new Error(`invalid option: ${key}`);
result[key.slice(2)] = process.argv[i + 1];
}
if (!result.input || !result.output || !result.properties)
throw new Error("Usage: source_instrumentor.js --input FILE --output FILE --properties name[,name]");
return result;
}
function maskStringsAndComments(code) {
const chars = code.split("");
let state = "code";
let quote = "";
for (let i = 0; i < code.length; i += 1) {
const ch = code[i];
const next = code[i + 1];
if (state === "code") {
if (ch === "/" && next === "/") { chars[i] = " "; chars[i + 1] = " "; i += 1; state = "line"; continue; }
if (ch === "/" && next === "*") { chars[i] = " "; chars[i + 1] = " "; i += 1; state = "block"; continue; }
if (ch === "'" || ch === '"' || ch === "`") { quote = ch; chars[i] = " "; state = "string"; }
continue;
}
if (state === "line") {
if (ch === "\n") state = "code"; else chars[i] = " ";
continue;
}
if (state === "block") {
if (ch === "*" && next === "/") { chars[i] = " "; chars[i + 1] = " "; i += 1; state = "code"; }
else if (ch !== "\n") chars[i] = " ";
continue;
}
if (state === "string") {
if (ch === "\\") { chars[i] = " "; if (i + 1 < code.length) { if (code[i + 1] !== "\n") chars[i + 1] = " "; i += 1; } continue; }
if (ch === quote) { chars[i] = " "; state = "code"; }
else if (ch !== "\n") chars[i] = " ";
}
}
return chars.join("");
}
function lineOf(code, offset) {
return code.slice(0, offset).split("\n").length;
}
function instrument(code, options) {
const objects = new Set((options.objects || "navigator,document,screen,window,location").split(",").map(x => x.trim()).filter(Boolean));
const properties = new Set(options.properties.split(",").map(x => x.trim()).filter(Boolean));
const masked = maskStringsAndComments(code);
// Supports a simple dot/bracket chain with non-nested call arguments in the
// middle. The final member is inspected separately so calls such as
// CryptoJS.AES.encrypt(...) are not wrapped.
const pattern = /\b([A-Za-z_$][\w$]*)\s*((?:(?:\.\s*[A-Za-z_$][\w$]*\s*(?:\([^()\n]*\))?)|(?:\[\s*["'][A-Za-z_$][\w$]*["']\s*\]\s*(?:\([^()\n]*\))?))+)/g;
const edits = [];
let match;
while ((match = pattern.exec(code))) {
// The matching pass runs on source so static bracket names remain visible;
// reject starts that are actually inside a string/comment.
if (masked[match.index] !== code[match.index]) continue;
const objectName = match[1];
const expression = code.slice(match.index, match.index + match[0].length).trim();
const finalMember = /(?:\.\s*([A-Za-z_$][\w$]*)\s*(\([^()\n]*\))?|\[\s*["']([A-Za-z_$][\w$]*)["']\s*\]\s*(\([^()\n]*\))?)\s*$/.exec(expression);
if (!finalMember) continue;
const propertyName = finalMember[1] || finalMember[3];
if (!objects.has(objectName) || !properties.has(propertyName)) continue;
if (finalMember[2] || finalMember[4]) continue;
const end = match.index + match[0].length;
const tail = masked.slice(end).match(/^\s*/)[0].length;
const next = masked[end + tail];
const after = masked[end + tail + 1];
if (next === "(" || (next === "=" && after !== "=" && after !== ">")) continue;
if (expression.includes("__JSRA_TAP_GET__")) continue;
const label = `${objectName}.${propertyName}`;
edits.push({
start: match.index,
end,
replacement: `__JSRA_TAP_GET__(() => (${expression}), ${JSON.stringify(label)}, ${JSON.stringify(options.tag || "source")})`,
line: lineOf(code, match.index),
property: label
});
if (edits.length >= Number(options.maxRewrites || 500)) break;
}
let output = code;
for (const edit of edits.slice().reverse()) output = output.slice(0, edit.start) + edit.replacement + output.slice(edit.end);
const bootstrap = `\n;(() => {\n const g = globalThis;\n if (g.__JSRA_TAPS__) g.__JSRA_TAPS__.installed = true;\n else {\n const state = g.__JSRA_TAPS__ = { probe_id: "source-tap", installed: true, events: [], max: 5000 };\n g.__JSRA_TAP_GET__ = function(thunk, path, tag) {\n try { const value = thunk(); state.events.push({ type: "tap_get", path, tag, value_type: typeof value, value_length: value == null ? 0 : String(value).length, timestamp: Date.now() }); if (state.events.length > state.max) state.events.shift(); return value; }\n catch (error) { state.events.push({ type: "tap_get_err", path, tag, error: String(error), timestamp: Date.now() }); throw error; }\n };\n }\n})();\n`;
return { output: bootstrap + output, edits, parser_mode: "conservative-member-read" };
}
function main() {
const args = parseArgs();
const code = fs.readFileSync(args.input, "utf8");
const result = instrument(code, args);
fs.mkdirSync(path.dirname(args.output), { recursive: true });
fs.writeFileSync(args.output, result.output, "utf8");
const reportPath = args.report || `${args.output}.report.json`;
fs.writeFileSync(reportPath, JSON.stringify({
input: path.resolve(args.input),
output: path.resolve(args.output),
parser_mode: result.parser_mode,
rewrite_count: result.edits.length,
edits: result.edits.map(({ replacement, ...edit }) => edit)
}, null, 2) + "\n", "utf8");
console.log(JSON.stringify({ status: "ok", parser_mode: result.parser_mode, rewrite_count: result.edits.length, output: args.output, report: reportPath }));
}
try { main(); }
catch (error) { console.error(String(error.stack || error)); process.exit(2); }