253 lines
10 KiB
JavaScript
253 lines
10 KiB
JavaScript
"use strict";
|
|
/**
|
|
* ABE CLI — command-line interface for running explorations.
|
|
* Usage: abe run --url http://localhost:3000
|
|
*/
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || (function () {
|
|
var ownKeys = function(o) {
|
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
var ar = [];
|
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
return ar;
|
|
};
|
|
return ownKeys(o);
|
|
};
|
|
return function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
})();
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
const commander_1 = require("commander");
|
|
const ExplorationEngine_1 = require("./core/ExplorationEngine");
|
|
const StateGraph_1 = require("./core/StateGraph");
|
|
const PlaywrightAgent_1 = require("./plugins/agents/PlaywrightAgent");
|
|
const ScreenshotCollector_1 = require("./plugins/collectors/ScreenshotCollector");
|
|
const NetworkCollector_1 = require("./plugins/collectors/NetworkCollector");
|
|
const DOMSnapshotCollector_1 = require("./plugins/collectors/DOMSnapshotCollector");
|
|
const MarkdownExporter_1 = require("./plugins/exporters/MarkdownExporter");
|
|
const JSONExporter_1 = require("./plugins/exporters/JSONExporter");
|
|
const PlaywrightReproducer_1 = require("./plugins/reproducers/PlaywrightReproducer");
|
|
const ExplorationConfig_1 = require("./core/ExplorationConfig");
|
|
const fs = __importStar(require("fs"));
|
|
const path = __importStar(require("path"));
|
|
const program = new commander_1.Command();
|
|
program
|
|
.name('abe')
|
|
.description('Autonomous Bug Explorer — explore web apps and find bugs')
|
|
.version('0.1.0');
|
|
program
|
|
.command('run')
|
|
.description('Run an exploration session against a target URL')
|
|
.requiredOption('--url <url>', 'Target URL to explore')
|
|
.option('--seed <seed>', 'Deterministic seed', parseInt, 42)
|
|
.option('--max-states <n>', 'Max states to explore', parseInt, 50)
|
|
.option('--max-depth <n>', 'Max click depth', parseInt, 5)
|
|
.option('--allowed-domains <domains>', 'Comma-separated allowed domains')
|
|
.option('--excluded-paths <paths>', 'Comma-separated excluded paths')
|
|
.option('--action-delay <ms>', 'Delay between actions in ms', parseInt, 500)
|
|
.option('--session-timeout <ms>', 'Session timeout in ms', parseInt, 300000)
|
|
// Auth options
|
|
.option('--auth-type <type>', 'Auth type: cookies | headers | login_flow')
|
|
.option('--login-url <url>', 'Login page URL (for login_flow)')
|
|
.option('--username <user>', 'Username (for login_flow)')
|
|
.option('--password <pass>', 'Password (for login_flow)')
|
|
.option('--username-selector <sel>', 'Username field selector (for login_flow)')
|
|
.option('--password-selector <sel>', 'Password field selector (for login_flow)')
|
|
.option('--submit-selector <sel>', 'Submit button selector (for login_flow)')
|
|
// Output
|
|
.option('--output <format>', 'Output format: human | json | junit', 'human')
|
|
.option('--reports-dir <dir>', 'Output directory for reports', './reports')
|
|
// CI flags
|
|
.option('--fail-on-anomaly', 'Exit 1 if any anomaly found')
|
|
.option('--fail-on-severity <level>', 'Exit 1 if anomaly at or above severity found')
|
|
// Remote server
|
|
.option('--server <url>', 'Connect to remote ABE server instead of running inline')
|
|
.option('--api-key <key>', 'API key for remote server')
|
|
.action(async (opts) => {
|
|
const startMs = Date.now();
|
|
// Build auth config
|
|
let auth = null;
|
|
if (opts.authType === 'login_flow') {
|
|
auth = {
|
|
type: 'login_flow',
|
|
loginUrl: opts.loginUrl ?? '',
|
|
usernameSelector: opts.usernameSelector ?? 'input[type="email"]',
|
|
passwordSelector: opts.passwordSelector ?? 'input[type="password"]',
|
|
submitSelector: opts.submitSelector ?? 'button[type="submit"]',
|
|
username: opts.username ?? '',
|
|
password: opts.password ?? '',
|
|
};
|
|
}
|
|
else if (opts.authType === 'headers') {
|
|
auth = { type: 'headers', headers: {} };
|
|
}
|
|
else if (opts.authType === 'cookies') {
|
|
auth = { type: 'cookies', cookies: [] };
|
|
}
|
|
const config = {
|
|
...ExplorationConfig_1.DEFAULT_EXPLORATION_CONFIG,
|
|
maxStates: opts.maxStates,
|
|
maxDepth: opts.maxDepth,
|
|
actionDelayMs: opts.actionDelay,
|
|
sessionTimeoutMs: opts.sessionTimeout,
|
|
allowedDomains: opts.allowedDomains
|
|
? opts.allowedDomains.split(',').map((d) => d.trim())
|
|
: [new URL(opts.url).hostname],
|
|
excludedPaths: opts.excludedPaths
|
|
? opts.excludedPaths.split(',').map((p) => p.trim())
|
|
: [],
|
|
auth,
|
|
};
|
|
// If remote server mode
|
|
if (opts.server) {
|
|
await runRemote(opts, config);
|
|
return;
|
|
}
|
|
const anomalies = [];
|
|
let exitCode = 0;
|
|
let explorationError;
|
|
try {
|
|
const graph = new StateGraph_1.StateGraph();
|
|
const agent = new PlaywrightAgent_1.PlaywrightAgent({ seed: opts.seed, explorationConfig: config });
|
|
const engine = new ExplorationEngine_1.ExplorationEngine({
|
|
graph,
|
|
agent,
|
|
seed: opts.seed,
|
|
url: opts.url,
|
|
maxSteps: opts.maxStates,
|
|
outputDir: opts.reportsDir,
|
|
explorationConfig: config,
|
|
collectors: [
|
|
new ScreenshotCollector_1.ScreenshotCollector(opts.reportsDir),
|
|
new NetworkCollector_1.NetworkCollector(),
|
|
new DOMSnapshotCollector_1.DOMSnapshotCollector(opts.reportsDir),
|
|
],
|
|
exporters: [new MarkdownExporter_1.MarkdownExporter(), new JSONExporter_1.JSONExporter()],
|
|
reproducer: new PlaywrightReproducer_1.PlaywrightReproducer(),
|
|
events: {
|
|
onAnomalyDetected: (_, anomaly) => {
|
|
anomalies.push(anomaly);
|
|
},
|
|
onSessionError: (_, error) => {
|
|
explorationError = error;
|
|
},
|
|
},
|
|
});
|
|
await engine.run();
|
|
}
|
|
catch (err) {
|
|
explorationError = err instanceof Error ? err.message : String(err);
|
|
exitCode = 2;
|
|
}
|
|
if (explorationError && exitCode === 0)
|
|
exitCode = 2;
|
|
// Determine exit code from flags
|
|
if (exitCode === 0 && opts.failOnAnomaly && anomalies.length > 0) {
|
|
exitCode = 1;
|
|
}
|
|
if (exitCode === 0 && opts.failOnSeverity) {
|
|
const severityRank = { low: 0, medium: 1, high: 2, critical: 3 };
|
|
const threshold = severityRank[opts.failOnSeverity] ?? 0;
|
|
const failing = anomalies.filter((a) => (severityRank[a.severity] ?? 0) >= threshold);
|
|
if (failing.length > 0)
|
|
exitCode = 1;
|
|
}
|
|
const durationMs = Date.now() - startMs;
|
|
const summary = {
|
|
url: opts.url,
|
|
duration_ms: durationMs,
|
|
anomalies: anomalies.map((a) => ({
|
|
id: a.id,
|
|
type: a.type,
|
|
severity: a.severity,
|
|
description: a.description,
|
|
report_path: path.join(opts.reportsDir, a.id, 'report.json'),
|
|
})),
|
|
exit_code: exitCode,
|
|
};
|
|
if (opts.output === 'json') {
|
|
process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
|
|
}
|
|
else if (opts.output === 'junit') {
|
|
const xml = buildJunit(summary, opts.url);
|
|
const outPath = path.join(process.cwd(), 'abe-results.xml');
|
|
fs.writeFileSync(outPath, xml, 'utf8');
|
|
if (opts.output !== 'json') {
|
|
console.log(`JUnit results written to ${outPath}`);
|
|
}
|
|
}
|
|
else {
|
|
if (anomalies.length === 0) {
|
|
console.log(`✓ ABE finished. No anomalies found. (${durationMs}ms)`);
|
|
}
|
|
else {
|
|
console.log(`⚠ ABE finished. ${anomalies.length} anomaly(ies) found:`);
|
|
for (const a of anomalies) {
|
|
console.log(` [${a.severity.toUpperCase()}] ${a.type}: ${a.description}`);
|
|
}
|
|
}
|
|
}
|
|
process.exit(exitCode);
|
|
});
|
|
async function runRemote(opts, _config) {
|
|
const serverUrl = opts['server'];
|
|
const apiKey = opts['apiKey'];
|
|
const url = opts['url'];
|
|
const headers = { 'Content-Type': 'application/json' };
|
|
if (apiKey)
|
|
headers['x-abe-api-key'] = apiKey;
|
|
const res = await fetch(`${serverUrl}/api/sessions`, {
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify({ url }),
|
|
});
|
|
if (!res.ok) {
|
|
console.error(`Server error: ${res.status} ${await res.text()}`);
|
|
process.exit(2);
|
|
return;
|
|
}
|
|
const session = await res.json();
|
|
console.log(`Session started: ${session.sessionId}`);
|
|
process.exit(0);
|
|
}
|
|
function buildJunit(summary, url) {
|
|
const anomalyCount = summary.anomalies.length;
|
|
const cases = summary.anomalies
|
|
.map((a) => ` <testcase name="${escapeXml(a.description)}" classname="abe.anomaly.${escapeXml(a.type)}">\n` +
|
|
` <failure message="${escapeXml(a.description)}" type="${escapeXml(a.severity)}">${escapeXml(a.id)}</failure>\n` +
|
|
` </testcase>`)
|
|
.join('\n');
|
|
return `<?xml version="1.0" encoding="UTF-8"?>\n` +
|
|
`<testsuite name="ABE Exploration: ${escapeXml(url)}" tests="${anomalyCount}" failures="${anomalyCount}">\n` +
|
|
cases + '\n' +
|
|
`</testsuite>\n`;
|
|
}
|
|
function escapeXml(s) {
|
|
return s
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
program.parse(process.argv);
|