200 lines
6.0 KiB
JavaScript
Executable File
200 lines
6.0 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
/**
|
|
* End-to-end stealth regression check across key anti-bot targets.
|
|
*
|
|
* Usage:
|
|
* node scripts/check-stealth-regression.js
|
|
* node scripts/check-stealth-regression.js --binary ./cli/target/release/agent-browser
|
|
* node scripts/check-stealth-regression.js --session-name stealth-regression
|
|
*/
|
|
|
|
import { spawnSync } from 'node:child_process';
|
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const rootDir = join(__dirname, '..');
|
|
|
|
const args = process.argv.slice(2);
|
|
const getArgValue = (name, fallback) => {
|
|
const index = args.indexOf(name);
|
|
if (index === -1 || index + 1 >= args.length) return fallback;
|
|
return args[index + 1];
|
|
};
|
|
|
|
const sessionName = getArgValue('--session-name', 'stealth-regression');
|
|
const screenshotDir = getArgValue('--screenshot-dir', join('/tmp', 'agent-browser-stealth-regression'));
|
|
const binaryArg = getArgValue('--binary', '');
|
|
|
|
const candidates = [
|
|
binaryArg,
|
|
join(rootDir, 'cli', 'target', 'release', 'agent-browser'),
|
|
join(rootDir, 'bin', 'agent-browser.js'),
|
|
'agent-browser-stealth',
|
|
'agent-browser',
|
|
].filter(Boolean);
|
|
|
|
function tryResolveBinary() {
|
|
for (const candidate of candidates) {
|
|
if (candidate.includes('/') && !existsSync(candidate)) continue;
|
|
const probe = spawnSync(candidate, ['--version'], { encoding: 'utf8' });
|
|
if (probe.status === 0) return candidate;
|
|
}
|
|
throw new Error(
|
|
`Unable to find a runnable agent-browser binary. Tried: ${candidates.join(', ')}`
|
|
);
|
|
}
|
|
|
|
const binary = tryResolveBinary();
|
|
const sessionArgs = ['--session', sessionName, '--session-name', sessionName];
|
|
|
|
function runBinary(commandArgs, { allowFailure = false } = {}) {
|
|
const result = spawnSync(binary, commandArgs, {
|
|
encoding: 'utf8',
|
|
maxBuffer: 10 * 1024 * 1024,
|
|
});
|
|
if (result.status !== 0 && !allowFailure) {
|
|
const stderr = (result.stderr || '').trim();
|
|
const stdout = (result.stdout || '').trim();
|
|
throw new Error(
|
|
`Command failed: ${binary} ${commandArgs.join(' ')}\n` +
|
|
`${stderr || stdout || `exit code ${result.status}`}`
|
|
);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function runJson(actionArgs, options = {}) {
|
|
const result = runBinary([...sessionArgs, '--json', ...actionArgs], options);
|
|
const output = (result.stdout || '').trim();
|
|
if (!output) return null;
|
|
try {
|
|
return JSON.parse(output);
|
|
} catch {
|
|
throw new Error(`Expected JSON output, got:\n${output}`);
|
|
}
|
|
}
|
|
|
|
const genericRiskScript = `(() => {
|
|
const lowerTitle = String(document.title || '').toLowerCase();
|
|
const lowerBody = String(document.body?.innerText || '').toLowerCase();
|
|
const hasCloudflare =
|
|
lowerTitle.includes('just a moment') ||
|
|
lowerTitle.includes('performing security verification') ||
|
|
lowerBody.includes('performing security verification') ||
|
|
lowerBody.includes('checking your browser') ||
|
|
lowerBody.includes('cloudflare');
|
|
const hasCaptcha =
|
|
lowerBody.includes('captcha') ||
|
|
lowerBody.includes('recaptcha') ||
|
|
lowerBody.includes('hcaptcha') ||
|
|
lowerBody.includes('turnstile');
|
|
return {
|
|
title: document.title || '',
|
|
url: location.href,
|
|
hasCloudflare,
|
|
hasCaptcha,
|
|
hasTurnstile:
|
|
!!document.querySelector('.cf-turnstile, iframe[src*="challenges.cloudflare.com"], [name="cf-turnstile-response"]'),
|
|
bodySample: lowerBody.slice(0, 600),
|
|
};
|
|
})()`;
|
|
|
|
const sannysoftScript = `(() => {
|
|
const normalize = (s) => String(s || '').replace(/\\s+/g, ' ').trim();
|
|
const rows = Array.from(document.querySelectorAll('tr'));
|
|
const failed = rows.filter((row) => /failed|fail/i.test(normalize(row.innerText)));
|
|
return {
|
|
failedCount: failed.length,
|
|
failedRows: failed.map((row) => normalize(row.innerText)),
|
|
navigatorWebdriver: navigator.webdriver,
|
|
navigatorVendor: navigator.vendor,
|
|
};
|
|
})()`;
|
|
|
|
function sanitizeFileSegment(input) {
|
|
return input.replace(/[^a-zA-Z0-9._-]+/g, '-');
|
|
}
|
|
|
|
function main() {
|
|
mkdirSync(screenshotDir, { recursive: true });
|
|
const timestamp = new Date().toISOString();
|
|
const targets = [
|
|
'https://bot.sannysoft.com/',
|
|
'https://chatgpt.com/',
|
|
'https://super86.cc/login',
|
|
];
|
|
|
|
runBinary([...sessionArgs, 'close'], { allowFailure: true });
|
|
|
|
const report = {
|
|
binary,
|
|
sessionName,
|
|
timestamp,
|
|
screenshotDir,
|
|
doctor: null,
|
|
targets: [],
|
|
ok: true,
|
|
};
|
|
|
|
try {
|
|
const doctorResp = runJson(['doctor']);
|
|
report.doctor = doctorResp?.data ?? null;
|
|
|
|
for (const target of targets) {
|
|
const entry = {
|
|
target,
|
|
open: null,
|
|
risk: null,
|
|
sannysoft: null,
|
|
screenshot: null,
|
|
ok: true,
|
|
error: null,
|
|
};
|
|
|
|
try {
|
|
const openResp = runJson(['open', target]);
|
|
entry.open = openResp?.data ?? null;
|
|
|
|
runJson(['wait', '3000'], { allowFailure: true });
|
|
|
|
const riskResp = runJson(['eval', genericRiskScript]);
|
|
entry.risk = riskResp?.data?.result ?? null;
|
|
|
|
if (target.includes('bot.sannysoft.com')) {
|
|
const sannysoftResp = runJson(['eval', sannysoftScript]);
|
|
entry.sannysoft = sannysoftResp?.data?.result ?? null;
|
|
if ((entry.sannysoft?.failedCount ?? 1) > 0) {
|
|
entry.ok = false;
|
|
}
|
|
} else if (entry.risk?.hasCloudflare || entry.risk?.hasCaptcha) {
|
|
entry.ok = false;
|
|
}
|
|
|
|
const host = sanitizeFileSegment(new URL(target).host);
|
|
const shotPath = join(
|
|
screenshotDir,
|
|
`${host}-${Date.now().toString(36)}.png`
|
|
);
|
|
runJson(['screenshot', '--full', shotPath], { allowFailure: true });
|
|
entry.screenshot = shotPath;
|
|
} catch (error) {
|
|
entry.ok = false;
|
|
entry.error = error instanceof Error ? error.message : String(error);
|
|
}
|
|
|
|
if (!entry.ok) report.ok = false;
|
|
report.targets.push(entry);
|
|
}
|
|
} finally {
|
|
runBinary([...sessionArgs, 'close'], { allowFailure: true });
|
|
}
|
|
|
|
console.log(JSON.stringify(report, null, 2));
|
|
process.exit(report.ok ? 0 : 1);
|
|
}
|
|
|
|
main();
|