@vulcn/driver-browser 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/browser.ts","../src/recorder.ts","../src/runner.ts","../src/crawler.ts"],"sourcesContent":["/**\n * @vulcn/driver-browser\n *\n * Browser recording driver for Vulcn.\n * Uses Playwright to record and replay web application interactions.\n *\n * Step types:\n * - browser.navigate - Navigate to a URL\n * - browser.click - Click an element\n * - browser.input - Type into an input field\n * - browser.keypress - Press a key\n * - browser.scroll - Scroll the page\n * - browser.wait - Wait for a duration\n */\n\nimport { z } from \"zod\";\nimport type {\n VulcnDriver,\n RecorderDriver,\n RunnerDriver,\n RecordingHandle,\n RecordOptions,\n CrawlOptions,\n Session,\n Step,\n RunContext,\n RunResult,\n} from \"@vulcn/engine\";\n\nimport { BrowserRecorder } from \"./recorder\";\nimport { BrowserRunner } from \"./runner\";\nimport { crawlAndBuildSessions } from \"./crawler\";\n\n/**\n * Browser driver configuration schema\n */\nexport const configSchema = z.object({\n /** Starting URL for recording */\n startUrl: z.string().url().optional(),\n\n /** Browser type */\n browser: z.enum([\"chromium\", \"firefox\", \"webkit\"]).default(\"chromium\"),\n\n /** Viewport size */\n viewport: z\n .object({\n width: z.number().default(1280),\n height: z.number().default(720),\n })\n .default({ width: 1280, height: 720 }),\n\n /** Run headless */\n headless: z.boolean().default(false),\n});\n\nexport type BrowserConfig = z.infer<typeof configSchema>;\n\n/**\n * Browser step types\n */\nexport const BROWSER_STEP_TYPES = [\n \"browser.navigate\",\n \"browser.click\",\n \"browser.input\",\n \"browser.keypress\",\n \"browser.scroll\",\n \"browser.wait\",\n] as const;\n\nexport type BrowserStepType = (typeof BROWSER_STEP_TYPES)[number];\n\n/**\n * Browser-specific step schemas\n */\nexport const BrowserStepSchema = z.discriminatedUnion(\"type\", [\n z.object({\n id: z.string(),\n type: z.literal(\"browser.navigate\"),\n url: z.string(),\n timestamp: z.number(),\n }),\n z.object({\n id: z.string(),\n type: z.literal(\"browser.click\"),\n selector: z.string(),\n position: z.object({ x: z.number(), y: z.number() }).optional(),\n timestamp: z.number(),\n }),\n z.object({\n id: z.string(),\n type: z.literal(\"browser.input\"),\n selector: z.string(),\n value: z.string(),\n injectable: z.boolean().default(true),\n timestamp: z.number(),\n }),\n z.object({\n id: z.string(),\n type: z.literal(\"browser.keypress\"),\n key: z.string(),\n modifiers: z.array(z.string()).optional(),\n timestamp: z.number(),\n }),\n z.object({\n id: z.string(),\n type: z.literal(\"browser.scroll\"),\n selector: z.string().optional(),\n position: z.object({ x: z.number(), y: z.number() }),\n timestamp: z.number(),\n }),\n z.object({\n id: z.string(),\n type: z.literal(\"browser.wait\"),\n duration: z.number(),\n timestamp: z.number(),\n }),\n]);\n\nexport type BrowserStep = z.infer<typeof BrowserStepSchema>;\n\n/**\n * Browser recorder implementation\n */\nconst recorderDriver: RecorderDriver = {\n async start(\n config: Record<string, unknown>,\n options: RecordOptions,\n ): Promise<RecordingHandle> {\n const parsedConfig = configSchema.parse(config);\n return BrowserRecorder.start(parsedConfig, options);\n },\n\n async crawl(\n config: Record<string, unknown>,\n options: CrawlOptions,\n ): Promise<Session[]> {\n const parsedConfig = configSchema.parse(config);\n return crawlAndBuildSessions(\n {\n startUrl: parsedConfig.startUrl ?? \"\",\n browser: parsedConfig.browser,\n headless: parsedConfig.headless,\n viewport: parsedConfig.viewport,\n },\n options,\n );\n },\n};\n\n/**\n * Browser runner implementation\n */\nconst runnerDriver: RunnerDriver = {\n async execute(session: Session, ctx: RunContext): Promise<RunResult> {\n return BrowserRunner.execute(session, ctx);\n },\n};\n\n/**\n * Browser driver for Vulcn\n */\nconst browserDriver: VulcnDriver = {\n name: \"browser\",\n version: \"0.1.0\",\n apiVersion: 1,\n description: \"Browser recording driver using Playwright\",\n configSchema,\n stepTypes: [...BROWSER_STEP_TYPES],\n recorder: recorderDriver,\n runner: runnerDriver,\n};\n\nexport default browserDriver;\n\n// Re-export utilities\nexport { BrowserRecorder } from \"./recorder\";\nexport { BrowserRunner } from \"./runner\";\nexport { crawlAndBuildSessions } from \"./crawler\";\nexport { launchBrowser, checkBrowsers, installBrowsers } from \"./browser\";\n","/**\n * Browser utilities for @vulcn/driver-browser\n * Smart browser launching with system browser fallback\n */\n\nimport { chromium, firefox, webkit, type Browser } from \"playwright\";\nimport { exec } from \"node:child_process\";\nimport { promisify } from \"node:util\";\n\nconst execAsync = promisify(exec);\n\nexport type BrowserType = \"chromium\" | \"firefox\" | \"webkit\";\n\nexport interface LaunchOptions {\n browser?: BrowserType;\n headless?: boolean;\n}\n\nexport interface BrowserLaunchResult {\n browser: Browser;\n channel?: string;\n}\n\nexport class BrowserNotFoundError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"BrowserNotFoundError\";\n }\n}\n\n/**\n * Launch a browser with smart fallback:\n * 1. Try system Chrome/Edge first (zero-install experience)\n * 2. Fall back to Playwright's bundled browsers\n */\nexport async function launchBrowser(\n options: LaunchOptions = {},\n): Promise<BrowserLaunchResult> {\n const browserType = options.browser ?? \"chromium\";\n const headless = options.headless ?? false;\n\n // For Chromium, try system browsers first\n if (browserType === \"chromium\") {\n // Try system Chrome\n try {\n const browser = await chromium.launch({\n channel: \"chrome\",\n headless,\n });\n return { browser, channel: \"chrome\" };\n } catch {\n // Chrome not available\n }\n\n // Try system Edge\n try {\n const browser = await chromium.launch({\n channel: \"msedge\",\n headless,\n });\n return { browser, channel: \"msedge\" };\n } catch {\n // Edge not available\n }\n\n // Fall back to Playwright's bundled Chromium\n try {\n const browser = await chromium.launch({ headless });\n return { browser, channel: \"chromium\" };\n } catch {\n throw new BrowserNotFoundError(\n \"No Chromium browser found. Install Chrome or run: vulcn install chromium\",\n );\n }\n }\n\n // Firefox\n if (browserType === \"firefox\") {\n try {\n const browser = await firefox.launch({ headless });\n return { browser, channel: \"firefox\" };\n } catch {\n throw new BrowserNotFoundError(\n \"Firefox not found. Run: vulcn install firefox\",\n );\n }\n }\n\n // WebKit\n if (browserType === \"webkit\") {\n try {\n const browser = await webkit.launch({ headless });\n return { browser, channel: \"webkit\" };\n } catch {\n throw new BrowserNotFoundError(\n \"WebKit not found. Run: vulcn install webkit\",\n );\n }\n }\n\n throw new BrowserNotFoundError(`Unknown browser type: ${browserType}`);\n}\n\n/**\n * Install Playwright browsers\n */\nexport async function installBrowsers(\n browsers: BrowserType[] = [\"chromium\"],\n): Promise<void> {\n const browserArg = browsers.join(\" \");\n await execAsync(`npx playwright install ${browserArg}`);\n}\n\n/**\n * Check which browsers are available\n */\nexport async function checkBrowsers(): Promise<{\n systemChrome: boolean;\n systemEdge: boolean;\n playwrightChromium: boolean;\n playwrightFirefox: boolean;\n playwrightWebkit: boolean;\n}> {\n const results = {\n systemChrome: false,\n systemEdge: false,\n playwrightChromium: false,\n playwrightFirefox: false,\n playwrightWebkit: false,\n };\n\n // Check system Chrome\n try {\n const browser = await chromium.launch({\n channel: \"chrome\",\n headless: true,\n });\n await browser.close();\n results.systemChrome = true;\n } catch {\n // Not available\n }\n\n // Check system Edge\n try {\n const browser = await chromium.launch({\n channel: \"msedge\",\n headless: true,\n });\n await browser.close();\n results.systemEdge = true;\n } catch {\n // Not available\n }\n\n // Check Playwright Chromium\n try {\n const browser = await chromium.launch({ headless: true });\n await browser.close();\n results.playwrightChromium = true;\n } catch {\n // Not installed\n }\n\n // Check Playwright Firefox\n try {\n const browser = await firefox.launch({ headless: true });\n await browser.close();\n results.playwrightFirefox = true;\n } catch {\n // Not installed\n }\n\n // Check Playwright WebKit\n try {\n const browser = await webkit.launch({ headless: true });\n await browser.close();\n results.playwrightWebkit = true;\n } catch {\n // Not installed\n }\n\n return results;\n}\n","/**\n * Browser Recorder Implementation\n *\n * Records browser interactions as replayable sessions.\n * Uses Playwright for browser automation.\n */\n\nimport type { Page } from \"playwright\";\nimport type {\n RecordingHandle,\n RecordOptions,\n Session,\n Step,\n} from \"@vulcn/engine\";\n\nimport { launchBrowser } from \"./browser\";\nimport type { BrowserConfig, BrowserStep } from \"./index\";\n\n/**\n * Browser Recorder - captures browser interactions\n */\nexport class BrowserRecorder {\n /**\n * Start a new recording session\n */\n static async start(\n config: BrowserConfig,\n _options: RecordOptions = {},\n ): Promise<RecordingHandle> {\n const { startUrl, browser: browserType, viewport, headless } = config;\n\n if (!startUrl) {\n throw new Error(\"startUrl is required for browser recording\");\n }\n\n // Launch browser with smart fallback\n const { browser } = await launchBrowser({\n browser: browserType,\n headless,\n });\n\n const context = await browser.newContext({ viewport });\n const page = await context.newPage();\n\n // Navigate to start URL\n await page.goto(startUrl);\n\n // Track recording\n const startTime = Date.now();\n const steps: Step[] = [];\n let stepCounter = 0;\n\n const generateStepId = () => {\n stepCounter++;\n return `step_${String(stepCounter).padStart(3, \"0\")}`;\n };\n\n // Add initial navigation step\n steps.push({\n id: generateStepId(),\n type: \"browser.navigate\",\n url: startUrl,\n timestamp: 0,\n });\n\n // Attach event listeners\n BrowserRecorder.attachListeners(page, steps, startTime, generateStepId);\n\n return {\n async stop(): Promise<Session> {\n const session: Session = {\n name: `Recording ${new Date().toISOString()}`,\n driver: \"browser\",\n driverConfig: {\n browser: browserType,\n viewport,\n startUrl,\n },\n steps,\n metadata: {\n recordedAt: new Date().toISOString(),\n version: \"1\",\n },\n };\n\n await browser.close();\n return session;\n },\n\n async abort(): Promise<void> {\n await browser.close();\n },\n\n getSteps(): Step[] {\n return [...steps];\n },\n\n addStep(step: Omit<Step, \"id\" | \"timestamp\">): void {\n steps.push({\n ...step,\n id: generateStepId(),\n timestamp: Date.now() - startTime,\n } as Step);\n },\n };\n }\n\n /**\n * Attach event listeners to the page\n */\n private static attachListeners(\n page: Page,\n steps: Step[],\n startTime: number,\n generateStepId: () => string,\n ) {\n const getTimestamp = () => Date.now() - startTime;\n\n const addStep = (\n step: Omit<Step, \"id\" | \"timestamp\"> & { type: string },\n ) => {\n steps.push({\n ...step,\n id: generateStepId(),\n timestamp: getTimestamp(),\n } as Step);\n };\n\n // Track navigation\n page.on(\"framenavigated\", (frame) => {\n if (frame === page.mainFrame()) {\n const url = frame.url();\n // Avoid duplicate nav steps for initial load\n const lastStep = steps[steps.length - 1];\n if (\n steps.length > 0 &&\n lastStep?.type === \"browser.navigate\" &&\n (lastStep as BrowserStep & { type: \"browser.navigate\" }).url === url\n ) {\n return;\n }\n addStep({\n type: \"browser.navigate\",\n url,\n });\n }\n });\n\n // Expose recording function to browser\n page.exposeFunction(\n \"__vulcn_record\",\n async (event: { type: string; data: Record<string, unknown> }) => {\n switch (event.type) {\n case \"click\": {\n const data = event.data as {\n selector: string;\n x: number;\n y: number;\n };\n addStep({\n type: \"browser.click\",\n selector: data.selector,\n position: { x: data.x, y: data.y },\n });\n break;\n }\n case \"input\": {\n const data = event.data as {\n selector: string;\n value: string;\n inputType: string | null;\n injectable: boolean;\n };\n addStep({\n type: \"browser.input\",\n selector: data.selector,\n value: data.value,\n injectable: data.injectable,\n });\n break;\n }\n case \"keypress\": {\n const data = event.data as { key: string; modifiers?: string[] };\n addStep({\n type: \"browser.keypress\",\n key: data.key,\n modifiers: data.modifiers,\n });\n break;\n }\n }\n },\n );\n\n // Inject recording script into every frame\n page.on(\"load\", async () => {\n await BrowserRecorder.injectRecordingScript(page);\n });\n\n // Inject into initial page\n BrowserRecorder.injectRecordingScript(page);\n }\n\n /**\n * Inject the recording script into the page\n */\n private static async injectRecordingScript(page: Page) {\n await page.evaluate(`\n (function() {\n if (window.__vulcn_injected) return;\n window.__vulcn_injected = true;\n\n var textInputTypes = ['text', 'password', 'email', 'search', 'url', 'tel', 'number'];\n\n function getSelector(el) {\n if (el.id) {\n return '#' + CSS.escape(el.id);\n }\n if (el.name) {\n var tag = el.tagName.toLowerCase();\n var nameSelector = tag + '[name=\"' + el.name + '\"]';\n if (document.querySelectorAll(nameSelector).length === 1) {\n return nameSelector;\n }\n }\n if (el.dataset && el.dataset.testid) {\n return '[data-testid=\"' + el.dataset.testid + '\"]';\n }\n if (el.tagName === 'INPUT' && el.type && el.name) {\n var inputSelector = 'input[type=\"' + el.type + '\"][name=\"' + el.name + '\"]';\n if (document.querySelectorAll(inputSelector).length === 1) {\n return inputSelector;\n }\n }\n if (el.className && typeof el.className === 'string') {\n var classes = el.className.trim().split(/\\\\s+/).filter(function(c) { return c.length > 0; });\n if (classes.length > 0) {\n var classSelector = el.tagName.toLowerCase() + '.' + classes.map(function(c) { return CSS.escape(c); }).join('.');\n if (document.querySelectorAll(classSelector).length === 1) {\n return classSelector;\n }\n }\n }\n var path = [];\n var current = el;\n while (current && current !== document.body) {\n var tag = current.tagName.toLowerCase();\n var parent = current.parentElement;\n if (parent) {\n var siblings = Array.from(parent.children).filter(function(c) { return c.tagName === current.tagName; });\n if (siblings.length > 1) {\n var index = siblings.indexOf(current) + 1;\n tag = tag + ':nth-of-type(' + index + ')';\n }\n }\n path.unshift(tag);\n current = parent;\n }\n return path.join(' > ');\n }\n\n function getInputType(el) {\n if (el.tagName === 'INPUT') return el.type || 'text';\n if (el.tagName === 'TEXTAREA') return 'textarea';\n if (el.tagName === 'SELECT') return 'select';\n return null;\n }\n\n function isTextInjectable(el) {\n var inputType = getInputType(el);\n if (!inputType) return false;\n if (inputType === 'textarea') return true;\n if (inputType === 'select') return false;\n return textInputTypes.indexOf(inputType) !== -1;\n }\n\n document.addEventListener('click', function(e) {\n var target = e.target;\n window.__vulcn_record({\n type: 'click',\n data: {\n selector: getSelector(target),\n x: e.clientX,\n y: e.clientY\n }\n });\n }, true);\n\n document.addEventListener('change', function(e) {\n var target = e.target;\n if ('value' in target) {\n var inputType = getInputType(target);\n window.__vulcn_record({\n type: 'input',\n data: {\n selector: getSelector(target),\n value: target.value,\n inputType: inputType,\n injectable: isTextInjectable(target)\n }\n });\n }\n }, true);\n\n document.addEventListener('keydown', function(e) {\n if (e.ctrlKey || e.metaKey || e.altKey) {\n var modifiers = [];\n if (e.ctrlKey) modifiers.push('ctrl');\n if (e.metaKey) modifiers.push('meta');\n if (e.altKey) modifiers.push('alt');\n if (e.shiftKey) modifiers.push('shift');\n\n window.__vulcn_record({\n type: 'keypress',\n data: {\n key: e.key,\n modifiers: modifiers\n }\n });\n }\n }, true);\n })();\n `);\n }\n}\n","/**\n * Browser Runner Implementation\n *\n * Replays browser sessions with security payloads.\n * Uses plugin hooks for detection.\n */\n\nimport type { Page, Dialog, ConsoleMessage } from \"playwright\";\nimport type {\n Session,\n Step,\n RunContext,\n RunResult,\n Finding,\n RuntimePayload,\n PayloadCategory,\n} from \"@vulcn/engine\";\n\nimport { launchBrowser, type BrowserType } from \"./browser\";\nimport type { BrowserStep } from \"./index\";\n\n/**\n * Browser Runner - replays sessions with payloads\n */\nexport class BrowserRunner {\n /**\n * Execute a session with security payloads\n */\n static async execute(session: Session, ctx: RunContext): Promise<RunResult> {\n const config = session.driverConfig;\n const browserType = (config.browser as BrowserType) ?? \"chromium\";\n const viewport = (config.viewport as { width: number; height: number }) ?? {\n width: 1280,\n height: 720,\n };\n const startUrl = config.startUrl as string;\n const headless = ctx.options.headless ?? true;\n\n const startTime = Date.now();\n const errors: string[] = [];\n let payloadsTested = 0;\n\n const payloads = ctx.payloads;\n if (payloads.length === 0) {\n return {\n findings: [],\n stepsExecuted: session.steps.length,\n payloadsTested: 0,\n duration: Date.now() - startTime,\n errors: [\n \"No payloads loaded. Add a payload plugin or configure payloads.\",\n ],\n };\n }\n\n // Launch browser\n const { browser } = await launchBrowser({\n browser: browserType,\n headless,\n });\n const context = await browser.newContext({ viewport });\n const page = await context.newPage();\n\n // Signal that the page is ready — plugins (e.g. passive scanner)\n // can now attach event listeners to the real page object\n await ctx.options.onPageReady?.(page);\n\n // Event findings from dialog/console handlers\n const eventFindings: Finding[] = [];\n let currentPayloadInfo: {\n stepId: string;\n payloadSet: RuntimePayload;\n payloadValue: string;\n } | null = null;\n\n // Dialog handler (for alert-based XSS detection)\n // ANY dialog triggered during payload testing is evidence of XSS execution\n const dialogHandler = async (dialog: Dialog) => {\n if (currentPayloadInfo) {\n const message = dialog.message();\n const dialogType = dialog.type();\n\n // Skip beforeunload dialogs (not XSS-related)\n if (dialogType !== \"beforeunload\") {\n eventFindings.push({\n type: \"xss\",\n severity: \"high\",\n title: `XSS Confirmed - ${dialogType}() triggered`,\n description: `JavaScript ${dialogType}() dialog was triggered by payload injection. Message: \"${message}\"`,\n stepId: currentPayloadInfo.stepId,\n payload: currentPayloadInfo.payloadValue,\n url: page.url(),\n evidence: `Dialog type: ${dialogType}, Message: ${message}`,\n metadata: {\n dialogType,\n dialogMessage: message,\n detectionMethod: \"dialog\",\n },\n });\n }\n }\n // Always dismiss dialogs to prevent blocking\n try {\n await dialog.dismiss();\n } catch {\n // Dialog may have already been handled\n }\n };\n\n // Console message handler\n const consoleHandler = async (msg: ConsoleMessage) => {\n if (currentPayloadInfo && msg.type() === \"log\") {\n const text = msg.text();\n if (\n text.includes(\"vulcn\") ||\n text.includes(currentPayloadInfo.payloadValue)\n ) {\n eventFindings.push({\n type: \"xss\",\n severity: \"high\",\n title: \"XSS Confirmed - Console Output\",\n description: `JavaScript console.log was triggered by payload injection`,\n stepId: currentPayloadInfo.stepId,\n payload: currentPayloadInfo.payloadValue,\n url: page.url(),\n evidence: `Console output: ${text}`,\n metadata: {\n consoleType: msg.type(),\n detectionMethod: \"console\",\n },\n });\n }\n }\n };\n\n page.on(\"dialog\", dialogHandler);\n page.on(\"console\", consoleHandler);\n\n try {\n // Find injectable steps (browser.input with injectable=true)\n const injectableSteps = session.steps.filter(\n (\n step,\n ): step is Step & { type: \"browser.input\"; injectable?: boolean } =>\n step.type === \"browser.input\" &&\n (step as BrowserStep & { type: \"browser.input\" }).injectable !==\n false,\n );\n\n // Build flat list of payloads, interleaved by category (round-robin).\n // This ensures we test at least one payload from each category quickly,\n // so the dedup early-break fires sooner on slow SPAs like Angular apps.\n // Before: [sqli1, sqli2, ..., sqli50, xss1, xss2, ..., xss50]\n // After: [sqli1, xss1, sqli2, xss2, ..., sqli50, xss50]\n const allPayloads: { payloadSet: RuntimePayload; value: string }[] = [];\n const payloadsByCategory = payloads.map((ps) =>\n ps.payloads.map((value) => ({ payloadSet: ps, value })),\n );\n const maxLen = Math.max(...payloadsByCategory.map((c) => c.length));\n for (let i = 0; i < maxLen; i++) {\n for (const category of payloadsByCategory) {\n if (i < category.length) {\n allPayloads.push(category[i]);\n }\n }\n }\n\n // Track confirmed vulnerability types per-step to avoid duplicate findings.\n // Once XSS is confirmed on an input (e.g., via dialog), skip remaining XSS payloads.\n const confirmedTypes = new Set<string>();\n\n // For each injectable step, test with each payload\n for (const injectableStep of injectableSteps) {\n for (const { payloadSet, value } of allPayloads) {\n // Skip if this vulnerability type is already confirmed for this step\n const stepTypeKey = `${injectableStep.id}::${payloadSet.category}`;\n if (confirmedTypes.has(stepTypeKey)) {\n continue;\n }\n\n try {\n currentPayloadInfo = {\n stepId: injectableStep.id,\n payloadSet,\n payloadValue: value,\n };\n\n // Replay session with payload\n await BrowserRunner.replayWithPayload(\n page,\n session,\n injectableStep,\n value,\n startUrl,\n );\n\n // Check for reflection\n const reflectionFinding = await BrowserRunner.checkReflection(\n page,\n injectableStep,\n payloadSet,\n value,\n );\n\n // Collect all findings from this payload\n const allFindings = [...eventFindings];\n if (reflectionFinding) {\n allFindings.push(reflectionFinding);\n }\n\n // Deduplicate: only add findings we haven't already reported\n const seenKeys = new Set<string>();\n for (const finding of allFindings) {\n const dedupKey = `${finding.type}::${finding.stepId}::${finding.title}`;\n if (!seenKeys.has(dedupKey)) {\n seenKeys.add(dedupKey);\n ctx.addFinding(finding);\n }\n }\n\n // If we got any finding (dialog, console, or reflection), mark as confirmed\n // and skip remaining payloads of this category for this input.\n // One confirmed finding is enough evidence — no need to test more payloads.\n if (allFindings.length > 0) {\n confirmedTypes.add(stepTypeKey);\n }\n\n // Clear event findings for next iteration\n eventFindings.length = 0;\n payloadsTested++;\n\n // Report progress\n ctx.options.onStepComplete?.(injectableStep.id, payloadsTested);\n } catch (err) {\n errors.push(`${injectableStep.id}: ${String(err)}`);\n }\n }\n }\n } finally {\n page.off(\"dialog\", dialogHandler);\n page.off(\"console\", consoleHandler);\n currentPayloadInfo = null;\n\n // Let plugins flush pending async work before browser closes\n // (e.g., passive scanner's in-flight response header analysis)\n await ctx.options.onBeforeClose?.(page);\n\n await browser.close();\n }\n\n return {\n findings: ctx.findings,\n stepsExecuted: session.steps.length,\n payloadsTested,\n duration: Date.now() - startTime,\n errors,\n };\n }\n\n /**\n * Replay session steps with payload injected at target step\n *\n * IMPORTANT: We replay ALL steps, not just up to the injectable step.\n * The injection replaces the input value, but subsequent steps (like\n * clicking submit) must still execute so the payload reaches the server\n * and gets reflected back in the response.\n */\n private static async replayWithPayload(\n page: Page,\n session: Session,\n targetStep: Step & { type: \"browser.input\" },\n payloadValue: string,\n startUrl: string,\n ): Promise<void> {\n // Navigate to start\n await page.goto(startUrl, { waitUntil: \"domcontentloaded\" });\n\n let injected = false;\n\n // Replay ALL steps — inject payload at the target input step,\n // but continue replaying remaining steps (clicks, navigations)\n // so forms get submitted and payloads reach the server\n for (const step of session.steps) {\n const browserStep = step as BrowserStep;\n\n try {\n switch (browserStep.type) {\n case \"browser.navigate\":\n // Skip post-submission navigates that have session-specific URLs\n // (they'll happen naturally from form submission)\n if (injected && browserStep.url.includes(\"sid=\")) {\n continue;\n }\n await page.goto(browserStep.url, { waitUntil: \"domcontentloaded\" });\n break;\n\n case \"browser.click\":\n // If this click is after injection, wait for potential navigation\n if (injected) {\n await Promise.all([\n page\n .waitForNavigation({\n waitUntil: \"domcontentloaded\",\n timeout: 5000,\n })\n .catch(() => {}),\n page.click(browserStep.selector, { timeout: 5000 }),\n ]);\n } else {\n await page.click(browserStep.selector, { timeout: 5000 });\n }\n break;\n\n case \"browser.input\": {\n // Inject payload for target step\n const value =\n step.id === targetStep.id ? payloadValue : browserStep.value;\n await page.fill(browserStep.selector, value, { timeout: 5000 });\n if (step.id === targetStep.id) {\n injected = true;\n }\n break;\n }\n\n case \"browser.keypress\": {\n const modifiers = browserStep.modifiers ?? [];\n for (const mod of modifiers) {\n await page.keyboard.down(\n mod as \"Control\" | \"Shift\" | \"Alt\" | \"Meta\",\n );\n }\n await page.keyboard.press(browserStep.key);\n for (const mod of modifiers.reverse()) {\n await page.keyboard.up(\n mod as \"Control\" | \"Shift\" | \"Alt\" | \"Meta\",\n );\n }\n break;\n }\n\n case \"browser.scroll\":\n if (browserStep.selector) {\n await page.locator(browserStep.selector).evaluate((el, pos) => {\n el.scrollTo(pos.x, pos.y);\n }, browserStep.position);\n } else {\n await page.evaluate((pos) => {\n window.scrollTo(pos.x, pos.y);\n }, browserStep.position);\n }\n break;\n\n case \"browser.wait\":\n await page.waitForTimeout(browserStep.duration);\n break;\n }\n } catch {\n // Step failed, continue to next\n }\n }\n\n // Wait for any scripts to execute after all steps complete\n await page.waitForTimeout(500);\n }\n\n /**\n * Check for payload reflection in page content\n */\n private static async checkReflection(\n page: Page,\n step: Step & { type: \"browser.input\" },\n payloadSet: RuntimePayload,\n payloadValue: string,\n ): Promise<Finding | undefined> {\n const content = await page.content();\n\n // Check for reflection patterns\n for (const pattern of payloadSet.detectPatterns) {\n if (pattern.test(content)) {\n return {\n type: payloadSet.category,\n severity: BrowserRunner.getSeverity(payloadSet.category),\n title: `${payloadSet.category.toUpperCase()} vulnerability detected`,\n description: `Payload pattern was reflected in page content`,\n stepId: step.id,\n payload: payloadValue,\n url: page.url(),\n evidence: content.match(pattern)?.[0]?.slice(0, 200),\n };\n }\n }\n\n // Check if payload appears verbatim\n if (content.includes(payloadValue)) {\n return {\n type: payloadSet.category,\n severity: \"medium\",\n title: `Potential ${payloadSet.category.toUpperCase()} - payload reflection`,\n description: `Payload was reflected in page without encoding`,\n stepId: step.id,\n payload: payloadValue,\n url: page.url(),\n };\n }\n\n return undefined;\n }\n\n /**\n * Determine severity based on vulnerability category\n */\n private static getSeverity(\n category: PayloadCategory,\n ): \"critical\" | \"high\" | \"medium\" | \"low\" | \"info\" {\n switch (category) {\n case \"sqli\":\n case \"command-injection\":\n case \"xxe\":\n return \"critical\";\n case \"xss\":\n case \"ssrf\":\n case \"path-traversal\":\n return \"high\";\n case \"open-redirect\":\n return \"medium\";\n default:\n return \"medium\";\n }\n }\n}\n","/**\n * Browser Crawler\n *\n * Automated session generator for the browser driver.\n * Crawls a web application using Playwright to discover:\n * - Forms with input fields and submit buttons\n * - Links to follow for deeper crawling\n *\n * Outputs Session[] that are directly compatible with BrowserRunner.\n *\n * This is the \"auto-record\" mode — instead of a human clicking around,\n * the crawler automatically discovers injection points.\n */\n\nimport type { Page, Browser, BrowserContext } from \"playwright\";\nimport type { Session, Step, CrawlOptions } from \"@vulcn/engine\";\nimport { launchBrowser, type BrowserType } from \"./browser\";\n\n// ── Internal Types ─────────────────────────────────────────────────────\n\ninterface DiscoveredInput {\n selector: string;\n type: string;\n name: string;\n injectable: boolean;\n placeholder?: string;\n}\n\ninterface DiscoveredForm {\n pageUrl: string;\n formSelector: string;\n action: string;\n method: string;\n inputs: DiscoveredInput[];\n submitSelector: string | null;\n}\n\n// Input types that accept text and can be injected with payloads\nconst INJECTABLE_INPUT_TYPES = new Set([\n \"text\",\n \"search\",\n \"url\",\n \"email\",\n \"tel\",\n \"password\",\n \"textarea\",\n \"\",\n]);\n\nconst CRAWL_DEFAULTS = {\n maxDepth: 2,\n maxPages: 20,\n pageTimeout: 10000,\n sameOrigin: true,\n};\n\n// ── Public API ─────────────────────────────────────────────────────────\n\nexport interface BrowserCrawlConfig {\n /** Starting URL */\n startUrl: string;\n /** Browser type */\n browser?: BrowserType;\n /** Run headless */\n headless?: boolean;\n /** Viewport */\n viewport?: { width: number; height: number };\n}\n\n/**\n * Crawl a URL and generate sessions.\n *\n * This is called by the browser driver's recorder.crawl() method.\n */\nexport async function crawlAndBuildSessions(\n config: BrowserCrawlConfig,\n options: CrawlOptions = {},\n): Promise<Session[]> {\n const opts = { ...CRAWL_DEFAULTS, ...options };\n const startUrl = config.startUrl;\n\n let normalizedUrl: URL;\n try {\n normalizedUrl = new URL(startUrl);\n } catch {\n throw new Error(`Invalid URL: ${startUrl}`);\n }\n\n const origin = normalizedUrl.origin;\n const visited = new Set<string>();\n const allForms: DiscoveredForm[] = [];\n\n // BFS queue: [url, depth]\n const queue: [string, number][] = [[normalizedUrl.href, 0]];\n\n // Use the shared browser launcher\n const { browser } = await launchBrowser({\n browser: config.browser ?? \"chromium\",\n headless: config.headless ?? true,\n });\n\n const context: BrowserContext = await browser.newContext({\n viewport: config.viewport ?? { width: 1280, height: 720 },\n });\n\n try {\n while (queue.length > 0 && visited.size < opts.maxPages) {\n const [url, depth] = queue.shift()!;\n const normalizedPageUrl = normalizeUrl(url);\n\n if (visited.has(normalizedPageUrl)) continue;\n visited.add(normalizedPageUrl);\n\n console.log(`[crawler] [depth=${depth}] Crawling: ${normalizedPageUrl}`);\n\n const page: Page = await context.newPage();\n\n try {\n await page.goto(normalizedPageUrl, {\n waitUntil: \"domcontentloaded\",\n timeout: opts.pageTimeout,\n });\n\n // Wait for JS-rendered content\n await page.waitForTimeout(1000);\n\n // Discover forms\n const forms = await discoverForms(page, normalizedPageUrl);\n allForms.push(...forms);\n\n const injectableCount = forms.reduce(\n (s, f) => s + f.inputs.filter((i) => i.injectable).length,\n 0,\n );\n console.log(\n `[crawler] Found ${forms.length} form(s), ${injectableCount} injectable input(s)`,\n );\n\n opts.onPageCrawled?.(normalizedPageUrl, forms.length);\n\n // Follow links\n if (depth < opts.maxDepth) {\n const links = await discoverLinks(page, origin, opts.sameOrigin);\n for (const link of links) {\n const normalizedLink = normalizeUrl(link);\n if (!visited.has(normalizedLink)) {\n queue.push([normalizedLink, depth + 1]);\n }\n }\n console.log(`[crawler] Found ${links.length} link(s) to follow`);\n }\n } catch (err) {\n console.warn(\n `[crawler] Failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n } finally {\n await page.close();\n }\n }\n } finally {\n await browser.close();\n }\n\n console.log(\n `[crawler] Complete: ${visited.size} page(s), ${allForms.length} form(s)`,\n );\n\n // Convert discovered forms to sessions\n return buildSessions(allForms);\n}\n\n// ── Form Discovery ─────────────────────────────────────────────────────\n\nasync function discoverForms(\n page: Page,\n pageUrl: string,\n): Promise<DiscoveredForm[]> {\n const forms: DiscoveredForm[] = [];\n\n // 1. Explicit <form> elements\n const explicitForms = await page.evaluate(() => {\n const results: Array<{\n formIndex: number;\n action: string;\n method: string;\n inputs: Array<{\n selector: string;\n type: string;\n name: string;\n placeholder: string;\n }>;\n submitSelector: string | null;\n }> = [];\n\n const formElements = document.querySelectorAll(\"form\");\n\n formElements.forEach((form, formIndex) => {\n const inputs: Array<{\n selector: string;\n type: string;\n name: string;\n placeholder: string;\n }> = [];\n\n const inputEls = form.querySelectorAll(\n 'input, textarea, [contenteditable=\"true\"]',\n );\n\n inputEls.forEach((input, inputIndex) => {\n const el = input as HTMLInputElement;\n const type =\n el.tagName.toLowerCase() === \"textarea\"\n ? \"textarea\"\n : el.getAttribute(\"type\") || \"text\";\n const name = el.name || el.id || `input-${inputIndex}`;\n\n let selector = \"\";\n if (el.id) {\n selector = `#${CSS.escape(el.id)}`;\n } else if (el.name) {\n selector = `form:nth-of-type(${formIndex + 1}) [name=\"${CSS.escape(el.name)}\"]`;\n } else {\n selector = `form:nth-of-type(${formIndex + 1}) ${el.tagName.toLowerCase()}:nth-of-type(${inputIndex + 1})`;\n }\n\n inputs.push({\n selector,\n type,\n name,\n placeholder: el.placeholder || \"\",\n });\n });\n\n // Find submit trigger\n let submitSelector: string | null = null;\n const submitBtn =\n form.querySelector('button[type=\"submit\"], input[type=\"submit\"]') ||\n form.querySelector(\"button:not([type])\") ||\n form.querySelector('button, input[type=\"button\"]');\n\n if (submitBtn) {\n const btn = submitBtn as HTMLElement;\n if (btn.id) {\n submitSelector = `#${CSS.escape(btn.id)}`;\n } else {\n const tag = btn.tagName.toLowerCase();\n const type = btn.getAttribute(\"type\");\n if (type) {\n submitSelector = `form:nth-of-type(${formIndex + 1}) ${tag}[type=\"${type}\"]`;\n } else {\n submitSelector = `form:nth-of-type(${formIndex + 1}) ${tag}`;\n }\n }\n }\n\n results.push({\n formIndex,\n action: form.action || \"\",\n method: (form.method || \"GET\").toUpperCase(),\n inputs,\n submitSelector,\n });\n });\n\n return results;\n });\n\n for (const form of explicitForms) {\n if (form.inputs.length === 0) continue;\n\n forms.push({\n pageUrl,\n formSelector: `form:nth-of-type(${form.formIndex + 1})`,\n action: form.action,\n method: form.method,\n inputs: form.inputs.map((input) => ({\n selector: input.selector,\n type: input.type,\n name: input.name,\n injectable: INJECTABLE_INPUT_TYPES.has(input.type.toLowerCase()),\n placeholder: input.placeholder || undefined,\n })),\n submitSelector: form.submitSelector,\n });\n }\n\n // 2. Standalone inputs NOT inside a <form>\n const standaloneInputs = await page.evaluate(() => {\n const results: Array<{\n selector: string;\n type: string;\n name: string;\n placeholder: string;\n nearbyButtonSelector: string | null;\n }> = [];\n\n const allInputs = document.querySelectorAll(\n 'input:not(form input), textarea:not(form textarea), [contenteditable=\"true\"]:not(form [contenteditable])',\n );\n\n allInputs.forEach((input) => {\n const el = input as HTMLInputElement;\n const type =\n el.tagName.toLowerCase() === \"textarea\"\n ? \"textarea\"\n : el.getAttribute(\"type\") || \"text\";\n const name = el.name || el.id || \"\";\n\n let selector = \"\";\n if (el.id) {\n selector = `#${CSS.escape(el.id)}`;\n } else if (el.name) {\n selector = `[name=\"${CSS.escape(el.name)}\"]`;\n } else {\n selector = `${el.tagName.toLowerCase()}[type=\"${type}\"]`;\n }\n\n // Look for nearby button\n let nearbyButtonSelector: string | null = null;\n const parent = el.parentElement;\n if (parent) {\n const btn =\n parent.querySelector(\"button\") ||\n parent.querySelector('input[type=\"submit\"]') ||\n parent.querySelector('input[type=\"button\"]');\n if (btn) {\n const btnEl = btn as HTMLElement;\n if (btnEl.id) {\n nearbyButtonSelector = `#${CSS.escape(btnEl.id)}`;\n }\n }\n }\n\n results.push({\n selector,\n type,\n name,\n placeholder: el.placeholder || \"\",\n nearbyButtonSelector,\n });\n });\n\n return results;\n });\n\n for (const input of standaloneInputs) {\n if (!INJECTABLE_INPUT_TYPES.has(input.type.toLowerCase())) continue;\n\n forms.push({\n pageUrl,\n formSelector: \"(standalone)\",\n action: pageUrl,\n method: \"GET\",\n inputs: [\n {\n selector: input.selector,\n type: input.type,\n name: input.name,\n injectable: true,\n placeholder: input.placeholder || undefined,\n },\n ],\n submitSelector: input.nearbyButtonSelector,\n });\n }\n\n return forms;\n}\n\n// ── Link Discovery ─────────────────────────────────────────────────────\n\n/** Query parameter names commonly used for external redirects */\nconst REDIRECT_PARAMS = new Set([\n \"to\",\n \"url\",\n \"redirect\",\n \"redirect_uri\",\n \"redirect_url\",\n \"return\",\n \"return_url\",\n \"returnto\",\n \"next\",\n \"goto\",\n \"dest\",\n \"destination\",\n \"continue\",\n \"target\",\n \"rurl\",\n \"out\",\n \"link\",\n \"forward\",\n]);\n\n/**\n * Check if a link is a same-origin redirect that points to an external URL.\n * Example: /redirect?to=https://github.com/...\n */\nfunction isExternalRedirectLink(link: string, origin: string): boolean {\n try {\n const parsed = new URL(link);\n // Only check links on our origin\n if (parsed.origin !== origin) return false;\n\n for (const [key, value] of parsed.searchParams) {\n if (REDIRECT_PARAMS.has(key.toLowerCase())) {\n // If the param value looks like an external URL, skip this link\n try {\n const targetUrl = new URL(value);\n if (targetUrl.origin !== origin) return true;\n } catch {\n // Not a URL — that's fine\n }\n }\n }\n return false;\n } catch {\n return false;\n }\n}\n\nasync function discoverLinks(\n page: Page,\n origin: string,\n sameOrigin: boolean,\n): Promise<string[]> {\n const links = await page.evaluate(() => {\n return Array.from(document.querySelectorAll(\"a[href]\"))\n .map((a) => (a as HTMLAnchorElement).href)\n .filter((href) => href.startsWith(\"http\"));\n });\n\n return links.filter((link) => {\n try {\n const linkOrigin = new URL(link).origin;\n // Filter out links to different origins\n if (sameOrigin && linkOrigin !== origin) return false;\n // Filter out redirect links that point to external URLs\n if (isExternalRedirectLink(link, origin)) return false;\n return true;\n } catch {\n return false;\n }\n });\n}\n\n// ── Session Builder ────────────────────────────────────────────────────\n\n/**\n * Convert discovered forms into Vulcn sessions.\n *\n * Each form with injectable inputs becomes one session:\n * navigate → fill input(s) → click submit / press Enter\n */\nfunction buildSessions(forms: DiscoveredForm[]): Session[] {\n const targetForms = forms.filter((f) => f.inputs.some((i) => i.injectable));\n\n return targetForms.map((form, idx) => buildSessionForForm(form, idx));\n}\n\nfunction buildSessionForForm(form: DiscoveredForm, index: number): Session {\n const steps: Step[] = [];\n let stepNum = 1;\n\n // Step 1: Navigate\n steps.push({\n id: `step-${stepNum++}`,\n type: \"browser.navigate\",\n url: form.pageUrl,\n timestamp: Date.now(),\n } as Step);\n\n // Steps 2+: Fill each injectable input\n const injectableInputs = form.inputs.filter((i) => i.injectable);\n\n for (const input of injectableInputs) {\n steps.push({\n id: `step-${stepNum++}`,\n type: \"browser.input\",\n selector: input.selector,\n value: \"test\",\n injectable: true,\n timestamp: Date.now() + stepNum * 100,\n } as Step);\n }\n\n // Final step: Submit\n if (form.submitSelector) {\n steps.push({\n id: `step-${stepNum++}`,\n type: \"browser.click\",\n selector: form.submitSelector,\n timestamp: Date.now() + stepNum * 100,\n } as Step);\n } else {\n // No submit button — press Enter\n steps.push({\n id: `step-${stepNum++}`,\n type: \"browser.keypress\",\n key: \"Enter\",\n timestamp: Date.now() + stepNum * 100,\n } as Step);\n }\n\n const inputNames = injectableInputs.map((i) => i.name || i.type).join(\", \");\n const pagePath = (() => {\n try {\n return new URL(form.pageUrl).pathname;\n } catch {\n return form.pageUrl;\n }\n })();\n\n return {\n name: `Crawl: ${pagePath} — form ${index + 1} (${inputNames})`,\n driver: \"browser\",\n driverConfig: {\n startUrl: form.pageUrl,\n browser: \"chromium\",\n headless: true,\n viewport: { width: 1280, height: 720 },\n },\n steps,\n metadata: {\n recordedAt: new Date().toISOString(),\n version: \"0.3.0\",\n source: \"crawler\",\n formAction: form.action,\n formMethod: form.method,\n },\n };\n}\n\n// ── Utilities ──────────────────────────────────────────────────────────\n\nfunction normalizeUrl(url: string): string {\n try {\n const parsed = new URL(url);\n parsed.hash = \"\";\n if (parsed.pathname !== \"/\" && parsed.pathname.endsWith(\"/\")) {\n parsed.pathname = parsed.pathname.slice(0, -1);\n }\n return parsed.href;\n } catch {\n return url;\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/browser.ts","../src/recorder.ts","../src/runner.ts","../src/crawler.ts","../src/auth.ts"],"sourcesContent":["/**\n * @vulcn/driver-browser\n *\n * Browser recording driver for Vulcn.\n * Uses Playwright to record and replay web application interactions.\n *\n * Step types:\n * - browser.navigate - Navigate to a URL\n * - browser.click - Click an element\n * - browser.input - Type into an input field\n * - browser.keypress - Press a key\n * - browser.scroll - Scroll the page\n * - browser.wait - Wait for a duration\n */\n\nimport { z } from \"zod\";\nimport type {\n VulcnDriver,\n RecorderDriver,\n RunnerDriver,\n RecordingHandle,\n RecordOptions,\n CrawlOptions,\n Session,\n Step,\n RunContext,\n RunResult,\n} from \"@vulcn/engine\";\n\nimport { BrowserRecorder } from \"./recorder\";\nimport { BrowserRunner } from \"./runner\";\nimport { crawlAndBuildSessions } from \"./crawler\";\n\n/**\n * Browser driver configuration schema\n */\nexport const configSchema = z.object({\n /** Starting URL for recording */\n startUrl: z.string().url().optional(),\n\n /** Browser type */\n browser: z.enum([\"chromium\", \"firefox\", \"webkit\"]).default(\"chromium\"),\n\n /** Viewport size */\n viewport: z\n .object({\n width: z.number().default(1280),\n height: z.number().default(720),\n })\n .default({ width: 1280, height: 720 }),\n\n /** Run headless */\n headless: z.boolean().default(false),\n});\n\nexport type BrowserConfig = z.infer<typeof configSchema>;\n\n/**\n * Browser step types\n */\nexport const BROWSER_STEP_TYPES = [\n \"browser.navigate\",\n \"browser.click\",\n \"browser.input\",\n \"browser.keypress\",\n \"browser.scroll\",\n \"browser.wait\",\n] as const;\n\nexport type BrowserStepType = (typeof BROWSER_STEP_TYPES)[number];\n\n/**\n * Browser-specific step schemas\n */\nexport const BrowserStepSchema = z.discriminatedUnion(\"type\", [\n z.object({\n id: z.string(),\n type: z.literal(\"browser.navigate\"),\n url: z.string(),\n timestamp: z.number(),\n }),\n z.object({\n id: z.string(),\n type: z.literal(\"browser.click\"),\n selector: z.string(),\n position: z.object({ x: z.number(), y: z.number() }).optional(),\n timestamp: z.number(),\n }),\n z.object({\n id: z.string(),\n type: z.literal(\"browser.input\"),\n selector: z.string(),\n value: z.string(),\n injectable: z.boolean().default(true),\n timestamp: z.number(),\n }),\n z.object({\n id: z.string(),\n type: z.literal(\"browser.keypress\"),\n key: z.string(),\n modifiers: z.array(z.string()).optional(),\n timestamp: z.number(),\n }),\n z.object({\n id: z.string(),\n type: z.literal(\"browser.scroll\"),\n selector: z.string().optional(),\n position: z.object({ x: z.number(), y: z.number() }),\n timestamp: z.number(),\n }),\n z.object({\n id: z.string(),\n type: z.literal(\"browser.wait\"),\n duration: z.number(),\n timestamp: z.number(),\n }),\n]);\n\nexport type BrowserStep = z.infer<typeof BrowserStepSchema>;\n\n/**\n * Browser recorder implementation\n */\nconst recorderDriver: RecorderDriver = {\n async start(\n config: Record<string, unknown>,\n options: RecordOptions,\n ): Promise<RecordingHandle> {\n const parsedConfig = configSchema.parse(config);\n return BrowserRecorder.start(parsedConfig, options);\n },\n\n async crawl(\n config: Record<string, unknown>,\n options: CrawlOptions,\n ): Promise<Session[]> {\n const parsedConfig = configSchema.parse(config);\n return crawlAndBuildSessions(\n {\n startUrl: parsedConfig.startUrl ?? \"\",\n browser: parsedConfig.browser,\n headless: parsedConfig.headless,\n viewport: parsedConfig.viewport,\n },\n options,\n );\n },\n};\n\n/**\n * Browser runner implementation\n */\nconst runnerDriver: RunnerDriver = {\n async execute(session: Session, ctx: RunContext): Promise<RunResult> {\n return BrowserRunner.execute(session, ctx);\n },\n};\n\n/**\n * Browser driver for Vulcn\n */\nconst browserDriver: VulcnDriver = {\n name: \"browser\",\n version: \"0.1.0\",\n apiVersion: 1,\n description: \"Browser recording driver using Playwright\",\n configSchema,\n stepTypes: [...BROWSER_STEP_TYPES],\n recorder: recorderDriver,\n runner: runnerDriver,\n};\n\nexport default browserDriver;\n\n// Re-export utilities\nexport { BrowserRecorder } from \"./recorder\";\nexport { BrowserRunner } from \"./runner\";\nexport { crawlAndBuildSessions } from \"./crawler\";\nexport { launchBrowser, checkBrowsers, installBrowsers } from \"./browser\";\nexport { detectLoginForm, performLogin, checkSessionAlive } from \"./auth\";\nexport type { LoginForm, LoginResult } from \"./auth\";\n","/**\n * Browser utilities for @vulcn/driver-browser\n * Smart browser launching with system browser fallback\n */\n\nimport { chromium, firefox, webkit, type Browser } from \"playwright\";\nimport { exec } from \"node:child_process\";\nimport { promisify } from \"node:util\";\n\nconst execAsync = promisify(exec);\n\nexport type BrowserType = \"chromium\" | \"firefox\" | \"webkit\";\n\nexport interface LaunchOptions {\n browser?: BrowserType;\n headless?: boolean;\n}\n\nexport interface BrowserLaunchResult {\n browser: Browser;\n channel?: string;\n}\n\nexport class BrowserNotFoundError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"BrowserNotFoundError\";\n }\n}\n\n/**\n * Launch a browser with smart fallback:\n * 1. Try system Chrome/Edge first (zero-install experience)\n * 2. Fall back to Playwright's bundled browsers\n */\nexport async function launchBrowser(\n options: LaunchOptions = {},\n): Promise<BrowserLaunchResult> {\n const browserType = options.browser ?? \"chromium\";\n const headless = options.headless ?? false;\n\n // For Chromium, try system browsers first\n if (browserType === \"chromium\") {\n // Try system Chrome\n try {\n const browser = await chromium.launch({\n channel: \"chrome\",\n headless,\n });\n return { browser, channel: \"chrome\" };\n } catch {\n // Chrome not available\n }\n\n // Try system Edge\n try {\n const browser = await chromium.launch({\n channel: \"msedge\",\n headless,\n });\n return { browser, channel: \"msedge\" };\n } catch {\n // Edge not available\n }\n\n // Fall back to Playwright's bundled Chromium\n try {\n const browser = await chromium.launch({ headless });\n return { browser, channel: \"chromium\" };\n } catch {\n throw new BrowserNotFoundError(\n \"No Chromium browser found. Install Chrome or run: vulcn install chromium\",\n );\n }\n }\n\n // Firefox\n if (browserType === \"firefox\") {\n try {\n const browser = await firefox.launch({ headless });\n return { browser, channel: \"firefox\" };\n } catch {\n throw new BrowserNotFoundError(\n \"Firefox not found. Run: vulcn install firefox\",\n );\n }\n }\n\n // WebKit\n if (browserType === \"webkit\") {\n try {\n const browser = await webkit.launch({ headless });\n return { browser, channel: \"webkit\" };\n } catch {\n throw new BrowserNotFoundError(\n \"WebKit not found. Run: vulcn install webkit\",\n );\n }\n }\n\n throw new BrowserNotFoundError(`Unknown browser type: ${browserType}`);\n}\n\n/**\n * Install Playwright browsers\n */\nexport async function installBrowsers(\n browsers: BrowserType[] = [\"chromium\"],\n): Promise<void> {\n const browserArg = browsers.join(\" \");\n await execAsync(`npx playwright install ${browserArg}`);\n}\n\n/**\n * Check which browsers are available\n */\nexport async function checkBrowsers(): Promise<{\n systemChrome: boolean;\n systemEdge: boolean;\n playwrightChromium: boolean;\n playwrightFirefox: boolean;\n playwrightWebkit: boolean;\n}> {\n const results = {\n systemChrome: false,\n systemEdge: false,\n playwrightChromium: false,\n playwrightFirefox: false,\n playwrightWebkit: false,\n };\n\n // Check system Chrome\n try {\n const browser = await chromium.launch({\n channel: \"chrome\",\n headless: true,\n });\n await browser.close();\n results.systemChrome = true;\n } catch {\n // Not available\n }\n\n // Check system Edge\n try {\n const browser = await chromium.launch({\n channel: \"msedge\",\n headless: true,\n });\n await browser.close();\n results.systemEdge = true;\n } catch {\n // Not available\n }\n\n // Check Playwright Chromium\n try {\n const browser = await chromium.launch({ headless: true });\n await browser.close();\n results.playwrightChromium = true;\n } catch {\n // Not installed\n }\n\n // Check Playwright Firefox\n try {\n const browser = await firefox.launch({ headless: true });\n await browser.close();\n results.playwrightFirefox = true;\n } catch {\n // Not installed\n }\n\n // Check Playwright WebKit\n try {\n const browser = await webkit.launch({ headless: true });\n await browser.close();\n results.playwrightWebkit = true;\n } catch {\n // Not installed\n }\n\n return results;\n}\n","/**\n * Browser Recorder Implementation\n *\n * Records browser interactions as replayable sessions.\n * Uses Playwright for browser automation.\n */\n\nimport type { Page } from \"playwright\";\nimport type {\n RecordingHandle,\n RecordOptions,\n Session,\n Step,\n} from \"@vulcn/engine\";\n\nimport { launchBrowser } from \"./browser\";\nimport type { BrowserConfig, BrowserStep } from \"./index\";\n\n/**\n * Browser Recorder - captures browser interactions\n */\nexport class BrowserRecorder {\n /**\n * Start a new recording session\n */\n static async start(\n config: BrowserConfig,\n _options: RecordOptions = {},\n ): Promise<RecordingHandle> {\n const { startUrl, browser: browserType, viewport, headless } = config;\n\n if (!startUrl) {\n throw new Error(\"startUrl is required for browser recording\");\n }\n\n // Launch browser with smart fallback\n const { browser } = await launchBrowser({\n browser: browserType,\n headless,\n });\n\n const context = await browser.newContext({ viewport });\n const page = await context.newPage();\n\n // Navigate to start URL\n await page.goto(startUrl);\n\n // Track recording\n const startTime = Date.now();\n const steps: Step[] = [];\n let stepCounter = 0;\n\n const generateStepId = () => {\n stepCounter++;\n return `step_${String(stepCounter).padStart(3, \"0\")}`;\n };\n\n // Add initial navigation step\n steps.push({\n id: generateStepId(),\n type: \"browser.navigate\",\n url: startUrl,\n timestamp: 0,\n });\n\n // Attach event listeners\n BrowserRecorder.attachListeners(page, steps, startTime, generateStepId);\n\n return {\n async stop(): Promise<Session> {\n const session: Session = {\n name: `Recording ${new Date().toISOString()}`,\n driver: \"browser\",\n driverConfig: {\n browser: browserType,\n viewport,\n startUrl,\n },\n steps,\n metadata: {\n recordedAt: new Date().toISOString(),\n version: \"1\",\n },\n };\n\n await browser.close();\n return session;\n },\n\n async abort(): Promise<void> {\n await browser.close();\n },\n\n getSteps(): Step[] {\n return [...steps];\n },\n\n addStep(step: Omit<Step, \"id\" | \"timestamp\">): void {\n steps.push({\n ...step,\n id: generateStepId(),\n timestamp: Date.now() - startTime,\n } as Step);\n },\n };\n }\n\n /**\n * Attach event listeners to the page\n */\n private static attachListeners(\n page: Page,\n steps: Step[],\n startTime: number,\n generateStepId: () => string,\n ) {\n const getTimestamp = () => Date.now() - startTime;\n\n const addStep = (\n step: Omit<Step, \"id\" | \"timestamp\"> & { type: string },\n ) => {\n steps.push({\n ...step,\n id: generateStepId(),\n timestamp: getTimestamp(),\n } as Step);\n };\n\n // Track navigation\n page.on(\"framenavigated\", (frame) => {\n if (frame === page.mainFrame()) {\n const url = frame.url();\n // Avoid duplicate nav steps for initial load\n const lastStep = steps[steps.length - 1];\n if (\n steps.length > 0 &&\n lastStep?.type === \"browser.navigate\" &&\n (lastStep as BrowserStep & { type: \"browser.navigate\" }).url === url\n ) {\n return;\n }\n addStep({\n type: \"browser.navigate\",\n url,\n });\n }\n });\n\n // Expose recording function to browser\n page.exposeFunction(\n \"__vulcn_record\",\n async (event: { type: string; data: Record<string, unknown> }) => {\n switch (event.type) {\n case \"click\": {\n const data = event.data as {\n selector: string;\n x: number;\n y: number;\n };\n addStep({\n type: \"browser.click\",\n selector: data.selector,\n position: { x: data.x, y: data.y },\n });\n break;\n }\n case \"input\": {\n const data = event.data as {\n selector: string;\n value: string;\n inputType: string | null;\n injectable: boolean;\n };\n addStep({\n type: \"browser.input\",\n selector: data.selector,\n value: data.value,\n injectable: data.injectable,\n });\n break;\n }\n case \"keypress\": {\n const data = event.data as { key: string; modifiers?: string[] };\n addStep({\n type: \"browser.keypress\",\n key: data.key,\n modifiers: data.modifiers,\n });\n break;\n }\n }\n },\n );\n\n // Inject recording script into every frame\n page.on(\"load\", async () => {\n await BrowserRecorder.injectRecordingScript(page);\n });\n\n // Inject into initial page\n BrowserRecorder.injectRecordingScript(page);\n }\n\n /**\n * Inject the recording script into the page\n */\n private static async injectRecordingScript(page: Page) {\n await page.evaluate(`\n (function() {\n if (window.__vulcn_injected) return;\n window.__vulcn_injected = true;\n\n var textInputTypes = ['text', 'password', 'email', 'search', 'url', 'tel', 'number'];\n\n function getSelector(el) {\n if (el.id) {\n return '#' + CSS.escape(el.id);\n }\n if (el.name) {\n var tag = el.tagName.toLowerCase();\n var nameSelector = tag + '[name=\"' + el.name + '\"]';\n if (document.querySelectorAll(nameSelector).length === 1) {\n return nameSelector;\n }\n }\n if (el.dataset && el.dataset.testid) {\n return '[data-testid=\"' + el.dataset.testid + '\"]';\n }\n if (el.tagName === 'INPUT' && el.type && el.name) {\n var inputSelector = 'input[type=\"' + el.type + '\"][name=\"' + el.name + '\"]';\n if (document.querySelectorAll(inputSelector).length === 1) {\n return inputSelector;\n }\n }\n if (el.className && typeof el.className === 'string') {\n var classes = el.className.trim().split(/\\\\s+/).filter(function(c) { return c.length > 0; });\n if (classes.length > 0) {\n var classSelector = el.tagName.toLowerCase() + '.' + classes.map(function(c) { return CSS.escape(c); }).join('.');\n if (document.querySelectorAll(classSelector).length === 1) {\n return classSelector;\n }\n }\n }\n var path = [];\n var current = el;\n while (current && current !== document.body) {\n var tag = current.tagName.toLowerCase();\n var parent = current.parentElement;\n if (parent) {\n var siblings = Array.from(parent.children).filter(function(c) { return c.tagName === current.tagName; });\n if (siblings.length > 1) {\n var index = siblings.indexOf(current) + 1;\n tag = tag + ':nth-of-type(' + index + ')';\n }\n }\n path.unshift(tag);\n current = parent;\n }\n return path.join(' > ');\n }\n\n function getInputType(el) {\n if (el.tagName === 'INPUT') return el.type || 'text';\n if (el.tagName === 'TEXTAREA') return 'textarea';\n if (el.tagName === 'SELECT') return 'select';\n return null;\n }\n\n function isTextInjectable(el) {\n var inputType = getInputType(el);\n if (!inputType) return false;\n if (inputType === 'textarea') return true;\n if (inputType === 'select') return false;\n return textInputTypes.indexOf(inputType) !== -1;\n }\n\n document.addEventListener('click', function(e) {\n var target = e.target;\n window.__vulcn_record({\n type: 'click',\n data: {\n selector: getSelector(target),\n x: e.clientX,\n y: e.clientY\n }\n });\n }, true);\n\n document.addEventListener('change', function(e) {\n var target = e.target;\n if ('value' in target) {\n var inputType = getInputType(target);\n window.__vulcn_record({\n type: 'input',\n data: {\n selector: getSelector(target),\n value: target.value,\n inputType: inputType,\n injectable: isTextInjectable(target)\n }\n });\n }\n }, true);\n\n document.addEventListener('keydown', function(e) {\n if (e.ctrlKey || e.metaKey || e.altKey) {\n var modifiers = [];\n if (e.ctrlKey) modifiers.push('ctrl');\n if (e.metaKey) modifiers.push('meta');\n if (e.altKey) modifiers.push('alt');\n if (e.shiftKey) modifiers.push('shift');\n\n window.__vulcn_record({\n type: 'keypress',\n data: {\n key: e.key,\n modifiers: modifiers\n }\n });\n }\n }, true);\n })();\n `);\n }\n}\n","/**\n * Browser Runner Implementation\n *\n * Replays browser sessions with security payloads.\n * Uses plugin hooks for detection.\n *\n * v2: Persistent browser with in-page payload cycling.\n * - ONE browser for the entire scan (not per-session)\n * - Uses page.goBack() between payloads instead of full page.goto()\n * - Falls back to full navigation when goBack() fails\n * - 5-10x faster on SPAs, same speed on simple sites\n */\n\nimport type {\n Browser,\n BrowserContext,\n Page,\n Dialog,\n ConsoleMessage,\n} from \"playwright\";\nimport type {\n Session,\n Step,\n RunContext,\n RunResult,\n Finding,\n RuntimePayload,\n PayloadCategory,\n} from \"@vulcn/engine\";\n\nimport { launchBrowser, type BrowserType } from \"./browser\";\nimport type { BrowserStep } from \"./index\";\n\n// ── Types ──────────────────────────────────────────────────────────────\n\ninterface PayloadItem {\n payloadSet: RuntimePayload;\n value: string;\n}\n\ninterface CurrentPayloadInfo {\n stepId: string;\n payloadSet: RuntimePayload;\n payloadValue: string;\n}\n\n// ── Browser Runner ─────────────────────────────────────────────────────\n\n/**\n * Browser Runner - replays sessions with payloads\n *\n * Supports two modes:\n * 1. Self-managed browser: launches its own browser (backward compat)\n * 2. Shared browser: receives a browser instance via RunOptions\n *\n * In both modes, payload cycling uses goBack() for speed.\n */\nexport class BrowserRunner {\n /**\n * Execute a session with security payloads.\n *\n * If ctx.options.browser is provided, reuses that browser (persistent mode).\n * Otherwise, launches and closes its own browser (standalone mode).\n */\n static async execute(session: Session, ctx: RunContext): Promise<RunResult> {\n const config = session.driverConfig;\n const browserType = (config.browser as BrowserType) ?? \"chromium\";\n const viewport = (config.viewport as { width: number; height: number }) ?? {\n width: 1280,\n height: 720,\n };\n const startUrl = config.startUrl as string;\n const headless = ctx.options.headless ?? true;\n\n const startTime = Date.now();\n const errors: string[] = [];\n let payloadsTested = 0;\n\n const payloads = ctx.payloads;\n if (payloads.length === 0) {\n return {\n findings: [],\n stepsExecuted: session.steps.length,\n payloadsTested: 0,\n duration: Date.now() - startTime,\n errors: [\n \"No payloads loaded. Add a payload plugin or configure payloads.\",\n ],\n };\n }\n\n // ── Browser lifecycle ────────────────────────────────────────────\n // Use shared browser from options if available (persistent mode),\n // otherwise launch our own (standalone / backward compat).\n const sharedBrowser = ctx.options.browser as Browser | undefined;\n const ownBrowser = sharedBrowser\n ? null\n : (await launchBrowser({ browser: browserType, headless })).browser;\n const browser = sharedBrowser ?? ownBrowser!;\n\n // Inject storageState from options if available (for auth)\n const storageState = ctx.options.storageState as string | undefined;\n const contextOptions: Record<string, unknown> = { viewport };\n if (storageState) {\n contextOptions.storageState = JSON.parse(storageState);\n }\n\n const context = await browser.newContext(contextOptions);\n const page = await context.newPage();\n\n // Signal that the page is ready — plugins (e.g. passive scanner)\n // can now attach event listeners to the real page object\n await ctx.options.onPageReady?.(page);\n\n // ── Event tracking ───────────────────────────────────────────────\n const eventFindings: Finding[] = [];\n let currentPayloadInfo: CurrentPayloadInfo | null = null;\n\n const dialogHandler = createDialogHandler(\n page,\n eventFindings,\n () => currentPayloadInfo,\n );\n const consoleHandler = createConsoleHandler(\n eventFindings,\n () => currentPayloadInfo,\n );\n\n page.on(\"dialog\", dialogHandler);\n page.on(\"console\", consoleHandler);\n\n try {\n // Find injectable steps (browser.input with injectable=true)\n const injectableSteps = session.steps.filter(\n (\n step,\n ): step is Step & { type: \"browser.input\"; injectable?: boolean } =>\n step.type === \"browser.input\" &&\n (step as BrowserStep & { type: \"browser.input\" }).injectable !==\n false,\n );\n\n // Build interleaved payload list (round-robin across categories)\n const allPayloads = interleavePayloads(payloads);\n\n // Track confirmed vulnerability types per-step (dedup)\n const confirmedTypes = new Set<string>();\n\n // ── Per-step payload cycling ───────────────────────────────────\n for (const injectableStep of injectableSteps) {\n let isFirstPayload = true;\n // Track the URL we navigate back to for goBack fallback\n let formPageUrl: string | null = null;\n\n for (const { payloadSet, value } of allPayloads) {\n // Skip if this vuln type is already confirmed for this step\n const stepTypeKey = `${injectableStep.id}::${payloadSet.category}`;\n if (confirmedTypes.has(stepTypeKey)) {\n continue;\n }\n\n try {\n currentPayloadInfo = {\n stepId: injectableStep.id,\n payloadSet,\n payloadValue: value,\n };\n\n if (isFirstPayload) {\n // First payload: full navigation + replay all steps\n await replayWithPayload(\n page,\n session,\n injectableStep,\n value,\n startUrl,\n );\n isFirstPayload = false;\n // Capture the form page URL for fallback navigation\n formPageUrl = startUrl;\n } else {\n // Subsequent payloads: try goBack() first (fast path)\n const cycled = await cyclePayload(\n page,\n session,\n injectableStep,\n value,\n formPageUrl ?? startUrl,\n );\n if (!cycled) {\n // goBack failed — fall back to full replay\n await replayWithPayload(\n page,\n session,\n injectableStep,\n value,\n startUrl,\n );\n }\n }\n\n // Check for reflection\n const reflectionFinding = await checkReflection(\n page,\n injectableStep,\n payloadSet,\n value,\n );\n\n // Collect all findings from this payload\n const allFindings = [...eventFindings];\n if (reflectionFinding) {\n allFindings.push(reflectionFinding);\n }\n\n // Deduplicate: only add findings we haven't already reported\n const seenKeys = new Set<string>();\n for (const finding of allFindings) {\n const dedupKey = `${finding.type}::${finding.stepId}::${finding.title}`;\n if (!seenKeys.has(dedupKey)) {\n seenKeys.add(dedupKey);\n ctx.addFinding(finding);\n }\n }\n\n // If we got any finding, mark as confirmed and skip remaining\n if (allFindings.length > 0) {\n confirmedTypes.add(stepTypeKey);\n }\n\n // Clear event findings for next iteration\n eventFindings.length = 0;\n payloadsTested++;\n\n // Report progress\n ctx.options.onStepComplete?.(injectableStep.id, payloadsTested);\n } catch (err) {\n errors.push(`${injectableStep.id}: ${String(err)}`);\n // If an error occurred, reset so next payload does full navigation\n isFirstPayload = true;\n }\n }\n }\n } finally {\n page.off(\"dialog\", dialogHandler);\n page.off(\"console\", consoleHandler);\n currentPayloadInfo = null;\n\n // Let plugins flush pending async work before browser closes\n await ctx.options.onBeforeClose?.(page);\n\n // Close context (lightweight — doesn't close the browser)\n await context.close();\n\n // Only close browser if we launched it ourselves\n if (ownBrowser) {\n await ownBrowser.close();\n }\n }\n\n return {\n findings: ctx.findings,\n stepsExecuted: session.steps.length,\n payloadsTested,\n duration: Date.now() - startTime,\n errors,\n };\n }\n}\n\n// ── Dialog / Console Handlers ──────────────────────────────────────────\n\nfunction createDialogHandler(\n page: Page,\n eventFindings: Finding[],\n getPayloadInfo: () => CurrentPayloadInfo | null,\n) {\n return async (dialog: Dialog) => {\n const info = getPayloadInfo();\n if (info) {\n const message = dialog.message();\n const dialogType = dialog.type();\n\n // Skip beforeunload dialogs (not XSS-related)\n if (dialogType !== \"beforeunload\") {\n eventFindings.push({\n type: \"xss\",\n severity: \"high\",\n title: `XSS Confirmed - ${dialogType}() triggered`,\n description: `JavaScript ${dialogType}() dialog was triggered by payload injection. Message: \"${message}\"`,\n stepId: info.stepId,\n payload: info.payloadValue,\n url: page.url(),\n evidence: `Dialog type: ${dialogType}, Message: ${message}`,\n metadata: {\n dialogType,\n dialogMessage: message,\n detectionMethod: \"dialog\",\n },\n });\n }\n }\n // Always dismiss dialogs to prevent blocking\n try {\n await dialog.dismiss();\n } catch {\n // Dialog may have already been handled\n }\n };\n}\n\nfunction createConsoleHandler(\n eventFindings: Finding[],\n getPayloadInfo: () => CurrentPayloadInfo | null,\n) {\n return async (msg: ConsoleMessage) => {\n const info = getPayloadInfo();\n if (info && msg.type() === \"log\") {\n const text = msg.text();\n if (text.includes(\"vulcn\") || text.includes(info.payloadValue)) {\n eventFindings.push({\n type: \"xss\",\n severity: \"high\",\n title: \"XSS Confirmed - Console Output\",\n description: `JavaScript console.log was triggered by payload injection`,\n stepId: info.stepId,\n payload: info.payloadValue,\n url: \"\",\n evidence: `Console output: ${text}`,\n metadata: {\n consoleType: msg.type(),\n detectionMethod: \"console\",\n },\n });\n }\n }\n };\n}\n\n// ── Payload Cycling ────────────────────────────────────────────────────\n\n/**\n * Fast-path payload cycling using goBack().\n *\n * After a form submission causes navigation, goBack() returns to the form\n * page much faster than a full page.goto() — especially for SPAs where\n * the client-side router handles the back navigation without a full reload.\n *\n * Falls back to full navigation if goBack doesn't land on a page with\n * the expected form.\n *\n * Returns true if cycling succeeded, false if caller should fall back\n * to full replay.\n */\nasync function cyclePayload(\n page: Page,\n session: Session,\n targetStep: Step & { type: \"browser.input\" },\n payloadValue: string,\n formPageUrl: string,\n): Promise<boolean> {\n try {\n // Try goBack first — fast on SPAs, cheap on static sites\n await page.goBack({ waitUntil: \"domcontentloaded\", timeout: 5000 });\n\n // Verify the form element is still present\n const targetSelector = (\n targetStep as BrowserStep & { type: \"browser.input\" }\n ).selector;\n const formPresent = await page\n .waitForSelector(targetSelector, { timeout: 3000 })\n .then(() => true)\n .catch(() => false);\n\n if (!formPresent) {\n // goBack didn't land on the form page — try direct navigation\n await page.goto(formPageUrl, {\n waitUntil: \"domcontentloaded\",\n timeout: 5000,\n });\n\n // Wait for the form element after navigation\n const formPresentAfterNav = await page\n .waitForSelector(targetSelector, { timeout: 3000 })\n .then(() => true)\n .catch(() => false);\n\n if (!formPresentAfterNav) {\n // Even direct nav failed — fall back to full replay\n return false;\n }\n }\n\n // Fill the injectable step with the new payload\n await page.fill(targetSelector, payloadValue, { timeout: 5000 });\n\n // Execute remaining steps after the injectable step (click submit, etc.)\n await replayStepsAfter(page, session, targetStep);\n\n // Wait for scripts to execute\n await page.waitForTimeout(500);\n\n return true;\n } catch {\n return false;\n }\n}\n\n// ── Full Replay ────────────────────────────────────────────────────────\n\n/**\n * Replay session steps with payload injected at target step.\n *\n * IMPORTANT: We replay ALL steps, not just up to the injectable step.\n * The injection replaces the input value, but subsequent steps (like\n * clicking submit) must still execute so the payload reaches the server\n * and gets reflected back in the response.\n */\nasync function replayWithPayload(\n page: Page,\n session: Session,\n targetStep: Step & { type: \"browser.input\" },\n payloadValue: string,\n startUrl: string,\n): Promise<void> {\n // Navigate to start\n await page.goto(startUrl, { waitUntil: \"domcontentloaded\" });\n\n let injected = false;\n\n for (const step of session.steps) {\n const browserStep = step as BrowserStep;\n\n try {\n switch (browserStep.type) {\n case \"browser.navigate\":\n if (injected && browserStep.url.includes(\"sid=\")) {\n continue;\n }\n await page.goto(browserStep.url, { waitUntil: \"domcontentloaded\" });\n break;\n\n case \"browser.click\":\n if (injected) {\n await Promise.all([\n page\n .waitForNavigation({\n waitUntil: \"domcontentloaded\",\n timeout: 5000,\n })\n .catch(() => {}),\n page.click(browserStep.selector, { timeout: 5000 }),\n ]);\n } else {\n await page.click(browserStep.selector, { timeout: 5000 });\n }\n break;\n\n case \"browser.input\": {\n const value =\n step.id === targetStep.id ? payloadValue : browserStep.value;\n await page.fill(browserStep.selector, value, { timeout: 5000 });\n if (step.id === targetStep.id) {\n injected = true;\n }\n break;\n }\n\n case \"browser.keypress\": {\n const modifiers = browserStep.modifiers ?? [];\n for (const mod of modifiers) {\n await page.keyboard.down(\n mod as \"Control\" | \"Shift\" | \"Alt\" | \"Meta\",\n );\n }\n await page.keyboard.press(browserStep.key);\n for (const mod of modifiers.reverse()) {\n await page.keyboard.up(mod as \"Control\" | \"Shift\" | \"Alt\" | \"Meta\");\n }\n break;\n }\n\n case \"browser.scroll\":\n if (browserStep.selector) {\n await page.locator(browserStep.selector).evaluate((el, pos) => {\n el.scrollTo(pos.x, pos.y);\n }, browserStep.position);\n } else {\n await page.evaluate((pos) => {\n window.scrollTo(pos.x, pos.y);\n }, browserStep.position);\n }\n break;\n\n case \"browser.wait\":\n await page.waitForTimeout(browserStep.duration);\n break;\n }\n } catch {\n // Step failed, continue to next\n }\n }\n\n // Wait for any scripts to execute after all steps complete\n await page.waitForTimeout(500);\n}\n\n/**\n * Replay only the steps AFTER the injectable step.\n *\n * Used by cyclePayload() — the injectable step has already been filled,\n * so we just need to execute the remaining steps (click submit, etc.)\n */\nasync function replayStepsAfter(\n page: Page,\n session: Session,\n targetStep: Step & { type: \"browser.input\" },\n): Promise<void> {\n let pastTarget = false;\n\n for (const step of session.steps) {\n if (step.id === targetStep.id) {\n pastTarget = true;\n continue; // skip the injectable step itself (already filled)\n }\n\n if (!pastTarget) continue;\n\n const browserStep = step as BrowserStep;\n\n try {\n switch (browserStep.type) {\n case \"browser.navigate\":\n // Skip post-submission navigates\n break;\n\n case \"browser.click\":\n await Promise.all([\n page\n .waitForNavigation({\n waitUntil: \"domcontentloaded\",\n timeout: 5000,\n })\n .catch(() => {}),\n page.click(browserStep.selector, { timeout: 5000 }),\n ]);\n break;\n\n case \"browser.input\":\n await page.fill(browserStep.selector, browserStep.value, {\n timeout: 5000,\n });\n break;\n\n case \"browser.keypress\": {\n const modifiers = browserStep.modifiers ?? [];\n for (const mod of modifiers) {\n await page.keyboard.down(\n mod as \"Control\" | \"Shift\" | \"Alt\" | \"Meta\",\n );\n }\n await page.keyboard.press(browserStep.key);\n for (const mod of modifiers.reverse()) {\n await page.keyboard.up(mod as \"Control\" | \"Shift\" | \"Alt\" | \"Meta\");\n }\n break;\n }\n\n case \"browser.scroll\":\n break; // skip scrolls in fast path\n\n case \"browser.wait\":\n break; // skip waits in fast path\n }\n } catch {\n // Step failed, continue\n }\n }\n\n // Wait for scripts to execute\n await page.waitForTimeout(500);\n}\n\n// ── Detection Helpers ──────────────────────────────────────────────────\n\n/**\n * Check for payload reflection in page content\n */\nasync function checkReflection(\n page: Page,\n step: Step & { type: \"browser.input\" },\n payloadSet: RuntimePayload,\n payloadValue: string,\n): Promise<Finding | undefined> {\n const content = await page.content();\n\n // Check for reflection patterns\n for (const pattern of payloadSet.detectPatterns) {\n if (pattern.test(content)) {\n return {\n type: payloadSet.category,\n severity: getSeverity(payloadSet.category),\n title: `${payloadSet.category.toUpperCase()} vulnerability detected`,\n description: `Payload pattern was reflected in page content`,\n stepId: step.id,\n payload: payloadValue,\n url: page.url(),\n evidence: content.match(pattern)?.[0]?.slice(0, 200),\n };\n }\n }\n\n // Check if payload appears verbatim\n if (content.includes(payloadValue)) {\n return {\n type: payloadSet.category,\n severity: \"medium\",\n title: `Potential ${payloadSet.category.toUpperCase()} - payload reflection`,\n description: `Payload was reflected in page without encoding`,\n stepId: step.id,\n payload: payloadValue,\n url: page.url(),\n };\n }\n\n return undefined;\n}\n\n/**\n * Determine severity based on vulnerability category\n */\nfunction getSeverity(\n category: PayloadCategory,\n): \"critical\" | \"high\" | \"medium\" | \"low\" | \"info\" {\n switch (category) {\n case \"sqli\":\n case \"command-injection\":\n case \"xxe\":\n return \"critical\";\n case \"xss\":\n case \"ssrf\":\n case \"path-traversal\":\n return \"high\";\n case \"open-redirect\":\n return \"medium\";\n default:\n return \"medium\";\n }\n}\n\n// ── Payload Interleaving ───────────────────────────────────────────────\n\n/**\n * Build flat list of payloads, interleaved by category (round-robin).\n *\n * This ensures we test at least one payload from each category quickly,\n * so the dedup early-break fires sooner on slow SPAs.\n *\n * Before: [sqli1, sqli2, ..., sqli50, xss1, xss2, ..., xss50]\n * After: [sqli1, xss1, sqli2, xss2, ..., sqli50, xss50]\n */\nfunction interleavePayloads(payloads: RuntimePayload[]): PayloadItem[] {\n const result: PayloadItem[] = [];\n const payloadsByCategory = payloads.map((ps) =>\n ps.payloads.map((value) => ({ payloadSet: ps, value })),\n );\n const maxLen = Math.max(...payloadsByCategory.map((c) => c.length));\n for (let i = 0; i < maxLen; i++) {\n for (const category of payloadsByCategory) {\n if (i < category.length) {\n result.push(category[i]);\n }\n }\n }\n return result;\n}\n","/**\n * Browser Crawler\n *\n * Automated session generator for the browser driver.\n * Crawls a web application using Playwright to discover:\n * - Forms with input fields and submit buttons\n * - Links to follow for deeper crawling\n *\n * Outputs Session[] that are directly compatible with BrowserRunner.\n *\n * This is the \"auto-record\" mode — instead of a human clicking around,\n * the crawler automatically discovers injection points.\n */\n\nimport type { Page, Browser, BrowserContext } from \"playwright\";\nimport type { Session, Step, CrawlOptions } from \"@vulcn/engine\";\nimport { launchBrowser, type BrowserType } from \"./browser\";\n\n// ── Internal Types ─────────────────────────────────────────────────────\n\ninterface DiscoveredInput {\n selector: string;\n type: string;\n name: string;\n injectable: boolean;\n placeholder?: string;\n}\n\ninterface DiscoveredForm {\n pageUrl: string;\n formSelector: string;\n action: string;\n method: string;\n inputs: DiscoveredInput[];\n submitSelector: string | null;\n}\n\n// Input types that accept text and can be injected with payloads\nconst INJECTABLE_INPUT_TYPES = new Set([\n \"text\",\n \"search\",\n \"url\",\n \"email\",\n \"tel\",\n \"password\",\n \"textarea\",\n \"\",\n]);\n\nconst CRAWL_DEFAULTS = {\n maxDepth: 2,\n maxPages: 20,\n pageTimeout: 10000,\n sameOrigin: true,\n};\n\n// ── Public API ─────────────────────────────────────────────────────────\n\nexport interface BrowserCrawlConfig {\n /** Starting URL */\n startUrl: string;\n /** Browser type */\n browser?: BrowserType;\n /** Run headless */\n headless?: boolean;\n /** Viewport */\n viewport?: { width: number; height: number };\n}\n\n/**\n * Crawl a URL and generate sessions.\n *\n * This is called by the browser driver's recorder.crawl() method.\n */\nexport async function crawlAndBuildSessions(\n config: BrowserCrawlConfig,\n options: CrawlOptions = {},\n): Promise<Session[]> {\n const opts = { ...CRAWL_DEFAULTS, ...options } as typeof CRAWL_DEFAULTS &\n CrawlOptions;\n const startUrl = config.startUrl;\n\n let normalizedUrl: URL;\n try {\n normalizedUrl = new URL(startUrl);\n } catch {\n throw new Error(`Invalid URL: ${startUrl}`);\n }\n\n const origin = normalizedUrl.origin;\n const visited = new Set<string>();\n const allForms: DiscoveredForm[] = [];\n\n // BFS queue: [url, depth]\n const queue: [string, number][] = [[normalizedUrl.href, 0]];\n\n // Use the shared browser launcher\n const { browser } = await launchBrowser({\n browser: config.browser ?? \"chromium\",\n headless: config.headless ?? true,\n });\n\n const context: BrowserContext = await browser.newContext({\n viewport: config.viewport ?? { width: 1280, height: 720 },\n ...(options.storageState\n ? { storageState: JSON.parse(options.storageState) }\n : {}),\n });\n\n try {\n while (queue.length > 0 && visited.size < opts.maxPages) {\n const [url, depth] = queue.shift()!;\n const normalizedPageUrl = normalizeUrl(url);\n\n if (visited.has(normalizedPageUrl)) continue;\n visited.add(normalizedPageUrl);\n\n console.log(`[crawler] [depth=${depth}] Crawling: ${normalizedPageUrl}`);\n\n const page: Page = await context.newPage();\n\n try {\n await page.goto(normalizedPageUrl, {\n waitUntil: \"domcontentloaded\",\n timeout: opts.pageTimeout,\n });\n\n // Wait for JS-rendered content\n await page.waitForTimeout(1000);\n\n // Discover forms\n const forms = await discoverForms(page, normalizedPageUrl);\n allForms.push(...forms);\n\n const injectableCount = forms.reduce(\n (s, f) => s + f.inputs.filter((i) => i.injectable).length,\n 0,\n );\n console.log(\n `[crawler] Found ${forms.length} form(s), ${injectableCount} injectable input(s)`,\n );\n\n opts.onPageCrawled?.(normalizedPageUrl, forms.length);\n\n // Follow links\n if (depth < opts.maxDepth) {\n const links = await discoverLinks(page, origin, opts.sameOrigin);\n for (const link of links) {\n const normalizedLink = normalizeUrl(link);\n if (!visited.has(normalizedLink)) {\n queue.push([normalizedLink, depth + 1]);\n }\n }\n console.log(`[crawler] Found ${links.length} link(s) to follow`);\n }\n } catch (err) {\n console.warn(\n `[crawler] Failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n } finally {\n await page.close();\n }\n }\n } finally {\n await browser.close();\n }\n\n console.log(\n `[crawler] Complete: ${visited.size} page(s), ${allForms.length} form(s)`,\n );\n\n // Convert discovered forms to sessions\n return buildSessions(allForms);\n}\n\n// ── Form Discovery ─────────────────────────────────────────────────────\n\nasync function discoverForms(\n page: Page,\n pageUrl: string,\n): Promise<DiscoveredForm[]> {\n const forms: DiscoveredForm[] = [];\n\n // 1. Explicit <form> elements\n const explicitForms = await page.evaluate(() => {\n const results: Array<{\n formIndex: number;\n action: string;\n method: string;\n inputs: Array<{\n selector: string;\n type: string;\n name: string;\n placeholder: string;\n }>;\n submitSelector: string | null;\n }> = [];\n\n const formElements = document.querySelectorAll(\"form\");\n\n formElements.forEach((form, formIndex) => {\n const inputs: Array<{\n selector: string;\n type: string;\n name: string;\n placeholder: string;\n }> = [];\n\n const inputEls = form.querySelectorAll(\n 'input, textarea, [contenteditable=\"true\"]',\n );\n\n inputEls.forEach((input, inputIndex) => {\n const el = input as HTMLInputElement;\n const type =\n el.tagName.toLowerCase() === \"textarea\"\n ? \"textarea\"\n : el.getAttribute(\"type\") || \"text\";\n const name = el.name || el.id || `input-${inputIndex}`;\n\n let selector = \"\";\n if (el.id) {\n selector = `#${CSS.escape(el.id)}`;\n } else if (el.name) {\n selector = `form:nth-of-type(${formIndex + 1}) [name=\"${CSS.escape(el.name)}\"]`;\n } else {\n selector = `form:nth-of-type(${formIndex + 1}) ${el.tagName.toLowerCase()}:nth-of-type(${inputIndex + 1})`;\n }\n\n inputs.push({\n selector,\n type,\n name,\n placeholder: el.placeholder || \"\",\n });\n });\n\n // Find submit trigger\n let submitSelector: string | null = null;\n const submitBtn =\n form.querySelector('button[type=\"submit\"], input[type=\"submit\"]') ||\n form.querySelector(\"button:not([type])\") ||\n form.querySelector('button, input[type=\"button\"]');\n\n if (submitBtn) {\n const btn = submitBtn as HTMLElement;\n if (btn.id) {\n submitSelector = `#${CSS.escape(btn.id)}`;\n } else {\n const tag = btn.tagName.toLowerCase();\n const type = btn.getAttribute(\"type\");\n if (type) {\n submitSelector = `form:nth-of-type(${formIndex + 1}) ${tag}[type=\"${type}\"]`;\n } else {\n submitSelector = `form:nth-of-type(${formIndex + 1}) ${tag}`;\n }\n }\n }\n\n results.push({\n formIndex,\n action: form.action || \"\",\n method: (form.method || \"GET\").toUpperCase(),\n inputs,\n submitSelector,\n });\n });\n\n return results;\n });\n\n for (const form of explicitForms) {\n if (form.inputs.length === 0) continue;\n\n forms.push({\n pageUrl,\n formSelector: `form:nth-of-type(${form.formIndex + 1})`,\n action: form.action,\n method: form.method,\n inputs: form.inputs.map((input) => ({\n selector: input.selector,\n type: input.type,\n name: input.name,\n injectable: INJECTABLE_INPUT_TYPES.has(input.type.toLowerCase()),\n placeholder: input.placeholder || undefined,\n })),\n submitSelector: form.submitSelector,\n });\n }\n\n // 2. Standalone inputs NOT inside a <form>\n const standaloneInputs = await page.evaluate(() => {\n const results: Array<{\n selector: string;\n type: string;\n name: string;\n placeholder: string;\n nearbyButtonSelector: string | null;\n }> = [];\n\n const allInputs = document.querySelectorAll(\n 'input:not(form input), textarea:not(form textarea), [contenteditable=\"true\"]:not(form [contenteditable])',\n );\n\n allInputs.forEach((input) => {\n const el = input as HTMLInputElement;\n const type =\n el.tagName.toLowerCase() === \"textarea\"\n ? \"textarea\"\n : el.getAttribute(\"type\") || \"text\";\n const name = el.name || el.id || \"\";\n\n let selector = \"\";\n if (el.id) {\n selector = `#${CSS.escape(el.id)}`;\n } else if (el.name) {\n selector = `[name=\"${CSS.escape(el.name)}\"]`;\n } else {\n selector = `${el.tagName.toLowerCase()}[type=\"${type}\"]`;\n }\n\n // Look for nearby button\n let nearbyButtonSelector: string | null = null;\n const parent = el.parentElement;\n if (parent) {\n const btn =\n parent.querySelector(\"button\") ||\n parent.querySelector('input[type=\"submit\"]') ||\n parent.querySelector('input[type=\"button\"]');\n if (btn) {\n const btnEl = btn as HTMLElement;\n if (btnEl.id) {\n nearbyButtonSelector = `#${CSS.escape(btnEl.id)}`;\n }\n }\n }\n\n results.push({\n selector,\n type,\n name,\n placeholder: el.placeholder || \"\",\n nearbyButtonSelector,\n });\n });\n\n return results;\n });\n\n for (const input of standaloneInputs) {\n if (!INJECTABLE_INPUT_TYPES.has(input.type.toLowerCase())) continue;\n\n forms.push({\n pageUrl,\n formSelector: \"(standalone)\",\n action: pageUrl,\n method: \"GET\",\n inputs: [\n {\n selector: input.selector,\n type: input.type,\n name: input.name,\n injectable: true,\n placeholder: input.placeholder || undefined,\n },\n ],\n submitSelector: input.nearbyButtonSelector,\n });\n }\n\n return forms;\n}\n\n// ── Link Discovery ─────────────────────────────────────────────────────\n\n/** Query parameter names commonly used for external redirects */\nconst REDIRECT_PARAMS = new Set([\n \"to\",\n \"url\",\n \"redirect\",\n \"redirect_uri\",\n \"redirect_url\",\n \"return\",\n \"return_url\",\n \"returnto\",\n \"next\",\n \"goto\",\n \"dest\",\n \"destination\",\n \"continue\",\n \"target\",\n \"rurl\",\n \"out\",\n \"link\",\n \"forward\",\n]);\n\n/**\n * Check if a link is a same-origin redirect that points to an external URL.\n * Example: /redirect?to=https://github.com/...\n */\nfunction isExternalRedirectLink(link: string, origin: string): boolean {\n try {\n const parsed = new URL(link);\n // Only check links on our origin\n if (parsed.origin !== origin) return false;\n\n for (const [key, value] of parsed.searchParams) {\n if (REDIRECT_PARAMS.has(key.toLowerCase())) {\n // If the param value looks like an external URL, skip this link\n try {\n const targetUrl = new URL(value);\n if (targetUrl.origin !== origin) return true;\n } catch {\n // Not a URL — that's fine\n }\n }\n }\n return false;\n } catch {\n return false;\n }\n}\n\nasync function discoverLinks(\n page: Page,\n origin: string,\n sameOrigin: boolean,\n): Promise<string[]> {\n const links = await page.evaluate(() => {\n return Array.from(document.querySelectorAll(\"a[href]\"))\n .map((a) => (a as HTMLAnchorElement).href)\n .filter((href) => href.startsWith(\"http\"));\n });\n\n return links.filter((link) => {\n try {\n const linkOrigin = new URL(link).origin;\n // Filter out links to different origins\n if (sameOrigin && linkOrigin !== origin) return false;\n // Filter out redirect links that point to external URLs\n if (isExternalRedirectLink(link, origin)) return false;\n return true;\n } catch {\n return false;\n }\n });\n}\n\n// ── Session Builder ────────────────────────────────────────────────────\n\n/**\n * Convert discovered forms into Vulcn sessions.\n *\n * Each form with injectable inputs becomes one session:\n * navigate → fill input(s) → click submit / press Enter\n */\nfunction buildSessions(forms: DiscoveredForm[]): Session[] {\n const targetForms = forms.filter((f) => f.inputs.some((i) => i.injectable));\n\n return targetForms.map((form, idx) => buildSessionForForm(form, idx));\n}\n\nfunction buildSessionForForm(form: DiscoveredForm, index: number): Session {\n const steps: Step[] = [];\n let stepNum = 1;\n\n // Step 1: Navigate\n steps.push({\n id: `step-${stepNum++}`,\n type: \"browser.navigate\",\n url: form.pageUrl,\n timestamp: Date.now(),\n } as Step);\n\n // Steps 2+: Fill each injectable input\n const injectableInputs = form.inputs.filter((i) => i.injectable);\n\n for (const input of injectableInputs) {\n steps.push({\n id: `step-${stepNum++}`,\n type: \"browser.input\",\n selector: input.selector,\n value: \"test\",\n injectable: true,\n timestamp: Date.now() + stepNum * 100,\n } as Step);\n }\n\n // Final step: Submit\n if (form.submitSelector) {\n steps.push({\n id: `step-${stepNum++}`,\n type: \"browser.click\",\n selector: form.submitSelector,\n timestamp: Date.now() + stepNum * 100,\n } as Step);\n } else {\n // No submit button — press Enter\n steps.push({\n id: `step-${stepNum++}`,\n type: \"browser.keypress\",\n key: \"Enter\",\n timestamp: Date.now() + stepNum * 100,\n } as Step);\n }\n\n const inputNames = injectableInputs.map((i) => i.name || i.type).join(\", \");\n const pagePath = (() => {\n try {\n return new URL(form.pageUrl).pathname;\n } catch {\n return form.pageUrl;\n }\n })();\n\n return {\n name: `Crawl: ${pagePath} — form ${index + 1} (${inputNames})`,\n driver: \"browser\",\n driverConfig: {\n startUrl: form.pageUrl,\n browser: \"chromium\",\n headless: true,\n viewport: { width: 1280, height: 720 },\n },\n steps,\n metadata: {\n recordedAt: new Date().toISOString(),\n version: \"0.3.0\",\n source: \"crawler\",\n formAction: form.action,\n formMethod: form.method,\n },\n };\n}\n\n// ── Utilities ──────────────────────────────────────────────────────────\n\nfunction normalizeUrl(url: string): string {\n try {\n const parsed = new URL(url);\n parsed.hash = \"\";\n if (parsed.pathname !== \"/\" && parsed.pathname.endsWith(\"/\")) {\n parsed.pathname = parsed.pathname.slice(0, -1);\n }\n return parsed.href;\n } catch {\n return url;\n }\n}\n","/**\n * Login Form Auto-Detection & Auth Replay\n *\n * Detects login forms on a page and fills them with credentials.\n * After login, captures the browser storage state (cookies + localStorage)\n * for re-use in subsequent scans.\n *\n * Detection strategy:\n * 1. Find forms with a password input (strongest signal)\n * 2. Find username field via heuristics (name, id, autocomplete, type)\n * 3. Find submit button\n * 4. Fall back to custom selectors from credentials\n */\n\nimport type { Page, BrowserContext } from \"playwright\";\nimport type { FormCredentials } from \"@vulcn/engine\";\n\n// ── Types ──────────────────────────────────────────────────────────────\n\nexport interface LoginForm {\n /** Username input selector */\n usernameSelector: string;\n /** Password input selector */\n passwordSelector: string;\n /** Submit button selector (may be null if not found) */\n submitSelector: string | null;\n /** Whether the form was detected automatically */\n autoDetected: boolean;\n}\n\nexport interface LoginResult {\n /** Whether login succeeded */\n success: boolean;\n /** Message for logging */\n message: string;\n /** Playwright storage state JSON (cookies + localStorage) */\n storageState?: string;\n}\n\n// ── Detection ──────────────────────────────────────────────────────────\n\n/**\n * Auto-detect a login form on the current page.\n *\n * Strategy:\n * 1. Find `<form>` elements containing an `input[type=\"password\"]`\n * 2. Within that form, find the username field using heuristics\n * 3. Find the submit button\n *\n * Falls back to page-wide search if no enclosing <form> is found.\n */\nexport async function detectLoginForm(page: Page): Promise<LoginForm | null> {\n return page.evaluate(() => {\n // Helper: find the best username input in a container\n function findUsernameInput(container: Element): HTMLInputElement | null {\n // Priority-ordered selectors for username fields\n const selectors = [\n 'input[autocomplete=\"username\"]',\n 'input[autocomplete=\"email\"]',\n 'input[type=\"email\"]',\n 'input[name*=\"user\" i]',\n 'input[name*=\"login\" i]',\n 'input[name*=\"email\" i]',\n 'input[id*=\"user\" i]',\n 'input[id*=\"login\" i]',\n 'input[id*=\"email\" i]',\n 'input[name*=\"name\" i]',\n 'input[type=\"text\"]',\n ];\n\n for (const sel of selectors) {\n const el = container.querySelector(sel) as HTMLInputElement | null;\n if (el && el.type !== \"password\" && el.type !== \"hidden\") {\n return el;\n }\n }\n\n return null;\n }\n\n // Helper: find submit button in a container\n function findSubmitButton(container: Element): HTMLElement | null {\n const selectors = [\n 'button[type=\"submit\"]',\n 'input[type=\"submit\"]',\n \"button:not([type])\",\n 'button[type=\"button\"]',\n ];\n\n for (const sel of selectors) {\n const el = container.querySelector(sel) as HTMLElement | null;\n if (el) return el;\n }\n\n return null;\n }\n\n // Helper: get a unique CSS selector for an element\n function getSelector(el: Element): string {\n if (el.id) return `#${CSS.escape(el.id)}`;\n if (el.getAttribute(\"name\"))\n return `${el.tagName.toLowerCase()}[name=\"${CSS.escape(el.getAttribute(\"name\")!)}\"]`;\n if (el.getAttribute(\"type\") && el.tagName === \"INPUT\")\n return `input[type=\"${el.getAttribute(\"type\")}\"]`;\n\n // Fall back to nth-child\n const parent = el.parentElement;\n if (!parent) return el.tagName.toLowerCase();\n const siblings = Array.from(parent.children);\n const index = siblings.indexOf(el) + 1;\n return `${parent.tagName.toLowerCase()} > ${el.tagName.toLowerCase()}:nth-child(${index})`;\n }\n\n // Strategy 1: Find <form> containing password input\n const forms = document.querySelectorAll(\"form\");\n for (const form of forms) {\n const passwordInput = form.querySelector(\n 'input[type=\"password\"]',\n ) as HTMLInputElement | null;\n if (!passwordInput) continue;\n\n const usernameInput = findUsernameInput(form);\n const submitButton = findSubmitButton(form);\n\n return {\n usernameSelector: usernameInput\n ? getSelector(usernameInput)\n : 'input[type=\"text\"]',\n passwordSelector: getSelector(passwordInput),\n submitSelector: submitButton ? getSelector(submitButton) : null,\n autoDetected: true,\n };\n }\n\n // Strategy 2: No <form> — look for password input anywhere on page\n const passwordInput = document.querySelector(\n 'input[type=\"password\"]',\n ) as HTMLInputElement | null;\n if (passwordInput) {\n const usernameInput = findUsernameInput(document.body);\n const submitButton = findSubmitButton(document.body);\n\n return {\n usernameSelector: usernameInput\n ? getSelector(usernameInput)\n : 'input[type=\"text\"]',\n passwordSelector: getSelector(passwordInput),\n submitSelector: submitButton ? getSelector(submitButton) : null,\n autoDetected: true,\n };\n }\n\n return null;\n });\n}\n\n// ── Login ──────────────────────────────────────────────────────────────\n\n/**\n * Perform login using detected form or custom selectors.\n *\n * Flow:\n * 1. Navigate to login URL (or target URL)\n * 2. Detect login form (or use custom selectors from credentials)\n * 3. Fill username + password\n * 4. Submit form\n * 5. Wait for navigation\n * 6. Check for logged-in indicator\n * 7. Capture storage state\n */\nexport async function performLogin(\n page: Page,\n context: BrowserContext,\n credentials: FormCredentials,\n options: {\n targetUrl: string;\n loggedInIndicator?: string;\n loggedOutIndicator?: string;\n },\n): Promise<LoginResult> {\n const loginUrl = credentials.loginUrl ?? options.targetUrl;\n\n // Navigate to login page\n await page.goto(loginUrl, { waitUntil: \"domcontentloaded\", timeout: 15000 });\n\n // Determine selectors — custom overrides or auto-detect\n let usernameSelector: string;\n let passwordSelector: string;\n let submitSelector: string | null;\n\n if (credentials.userSelector && credentials.passSelector) {\n // Use custom selectors\n usernameSelector = credentials.userSelector;\n passwordSelector = credentials.passSelector;\n submitSelector = null; // Will try to find automatically\n } else {\n // Auto-detect\n const form = await detectLoginForm(page);\n\n if (!form) {\n return {\n success: false,\n message: `No login form detected on ${loginUrl}. Use --user-field and --pass-field to specify selectors.`,\n };\n }\n\n usernameSelector = form.usernameSelector;\n passwordSelector = form.passwordSelector;\n submitSelector = form.submitSelector;\n }\n\n // Fill credentials\n try {\n await page.fill(usernameSelector, credentials.username, { timeout: 5000 });\n } catch {\n return {\n success: false,\n message: `Could not find username field: ${usernameSelector}`,\n };\n }\n\n try {\n await page.fill(passwordSelector, credentials.password, { timeout: 5000 });\n } catch {\n return {\n success: false,\n message: `Could not find password field: ${passwordSelector}`,\n };\n }\n\n // Submit form\n try {\n if (submitSelector) {\n await Promise.all([\n page\n .waitForNavigation({ waitUntil: \"domcontentloaded\", timeout: 10000 })\n .catch(() => {}),\n page.click(submitSelector, { timeout: 5000 }),\n ]);\n } else {\n // Try pressing Enter on the password field\n await Promise.all([\n page\n .waitForNavigation({ waitUntil: \"domcontentloaded\", timeout: 10000 })\n .catch(() => {}),\n page.press(passwordSelector, \"Enter\"),\n ]);\n }\n } catch {\n return {\n success: false,\n message: \"Failed to submit login form\",\n };\n }\n\n // Wait for page to settle\n await page.waitForTimeout(1000);\n\n // Check if login succeeded\n const bodyText = await page.textContent(\"body\").catch(() => \"\");\n\n if (\n options.loggedOutIndicator &&\n bodyText?.includes(options.loggedOutIndicator)\n ) {\n return {\n success: false,\n message: `Login failed — \"${options.loggedOutIndicator}\" still visible on page`,\n };\n }\n\n if (\n options.loggedInIndicator &&\n !bodyText?.includes(options.loggedInIndicator)\n ) {\n return {\n success: false,\n message: `Login uncertain — \"${options.loggedInIndicator}\" not found on page`,\n };\n }\n\n // Capture storage state (cookies + localStorage)\n const storageState = JSON.stringify(await context.storageState());\n\n return {\n success: true,\n message: \"Login successful\",\n storageState,\n };\n}\n\n// ── Session Expiry Detection ───────────────────────────────────────────\n\n/**\n * Check if the current session is still alive.\n *\n * Used during long-running scans to detect session expiry\n * and trigger re-authentication.\n */\nexport async function checkSessionAlive(\n page: Page,\n config: {\n loggedInIndicator?: string;\n loggedOutIndicator?: string;\n },\n): Promise<boolean> {\n try {\n const bodyText = await page.textContent(\"body\");\n if (!bodyText) return true; // Can't determine, assume alive\n\n // Check for logged-out indicator (strongest signal of expiry)\n if (\n config.loggedOutIndicator &&\n bodyText.includes(config.loggedOutIndicator)\n ) {\n return false;\n }\n\n // Check for missing logged-in indicator\n if (\n config.loggedInIndicator &&\n !bodyText.includes(config.loggedInIndicator)\n ) {\n return false;\n }\n\n return true;\n } catch {\n // Page error — assume session is dead\n return false;\n }\n}\n"],"mappings":"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","names":["passwordInput"]}
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
- import { RecordOptions, RecordingHandle, Session, RunContext, RunResult, CrawlOptions, VulcnDriver } from '@vulcn/engine';
3
- import { Browser } from 'playwright';
2
+ import { RecordOptions, RecordingHandle, Session, RunContext, RunResult, CrawlOptions, FormCredentials, VulcnDriver } from '@vulcn/engine';
3
+ import { Browser, Page, BrowserContext } from 'playwright';
4
4
 
5
5
  /**
6
6
  * Browser Recorder Implementation
@@ -32,33 +32,31 @@ declare class BrowserRecorder {
32
32
  *
33
33
  * Replays browser sessions with security payloads.
34
34
  * Uses plugin hooks for detection.
35
+ *
36
+ * v2: Persistent browser with in-page payload cycling.
37
+ * - ONE browser for the entire scan (not per-session)
38
+ * - Uses page.goBack() between payloads instead of full page.goto()
39
+ * - Falls back to full navigation when goBack() fails
40
+ * - 5-10x faster on SPAs, same speed on simple sites
35
41
  */
36
42
 
37
43
  /**
38
44
  * Browser Runner - replays sessions with payloads
45
+ *
46
+ * Supports two modes:
47
+ * 1. Self-managed browser: launches its own browser (backward compat)
48
+ * 2. Shared browser: receives a browser instance via RunOptions
49
+ *
50
+ * In both modes, payload cycling uses goBack() for speed.
39
51
  */
40
52
  declare class BrowserRunner {
41
53
  /**
42
- * Execute a session with security payloads
43
- */
44
- static execute(session: Session, ctx: RunContext): Promise<RunResult>;
45
- /**
46
- * Replay session steps with payload injected at target step
54
+ * Execute a session with security payloads.
47
55
  *
48
- * IMPORTANT: We replay ALL steps, not just up to the injectable step.
49
- * The injection replaces the input value, but subsequent steps (like
50
- * clicking submit) must still execute so the payload reaches the server
51
- * and gets reflected back in the response.
52
- */
53
- private static replayWithPayload;
54
- /**
55
- * Check for payload reflection in page content
56
+ * If ctx.options.browser is provided, reuses that browser (persistent mode).
57
+ * Otherwise, launches and closes its own browser (standalone mode).
56
58
  */
57
- private static checkReflection;
58
- /**
59
- * Determine severity based on vulnerability category
60
- */
61
- private static getSeverity;
59
+ static execute(session: Session, ctx: RunContext): Promise<RunResult>;
62
60
  }
63
61
 
64
62
  /**
@@ -130,6 +128,77 @@ interface BrowserCrawlConfig {
130
128
  */
131
129
  declare function crawlAndBuildSessions(config: BrowserCrawlConfig, options?: CrawlOptions): Promise<Session[]>;
132
130
 
131
+ /**
132
+ * Login Form Auto-Detection & Auth Replay
133
+ *
134
+ * Detects login forms on a page and fills them with credentials.
135
+ * After login, captures the browser storage state (cookies + localStorage)
136
+ * for re-use in subsequent scans.
137
+ *
138
+ * Detection strategy:
139
+ * 1. Find forms with a password input (strongest signal)
140
+ * 2. Find username field via heuristics (name, id, autocomplete, type)
141
+ * 3. Find submit button
142
+ * 4. Fall back to custom selectors from credentials
143
+ */
144
+
145
+ interface LoginForm {
146
+ /** Username input selector */
147
+ usernameSelector: string;
148
+ /** Password input selector */
149
+ passwordSelector: string;
150
+ /** Submit button selector (may be null if not found) */
151
+ submitSelector: string | null;
152
+ /** Whether the form was detected automatically */
153
+ autoDetected: boolean;
154
+ }
155
+ interface LoginResult {
156
+ /** Whether login succeeded */
157
+ success: boolean;
158
+ /** Message for logging */
159
+ message: string;
160
+ /** Playwright storage state JSON (cookies + localStorage) */
161
+ storageState?: string;
162
+ }
163
+ /**
164
+ * Auto-detect a login form on the current page.
165
+ *
166
+ * Strategy:
167
+ * 1. Find `<form>` elements containing an `input[type="password"]`
168
+ * 2. Within that form, find the username field using heuristics
169
+ * 3. Find the submit button
170
+ *
171
+ * Falls back to page-wide search if no enclosing <form> is found.
172
+ */
173
+ declare function detectLoginForm(page: Page): Promise<LoginForm | null>;
174
+ /**
175
+ * Perform login using detected form or custom selectors.
176
+ *
177
+ * Flow:
178
+ * 1. Navigate to login URL (or target URL)
179
+ * 2. Detect login form (or use custom selectors from credentials)
180
+ * 3. Fill username + password
181
+ * 4. Submit form
182
+ * 5. Wait for navigation
183
+ * 6. Check for logged-in indicator
184
+ * 7. Capture storage state
185
+ */
186
+ declare function performLogin(page: Page, context: BrowserContext, credentials: FormCredentials, options: {
187
+ targetUrl: string;
188
+ loggedInIndicator?: string;
189
+ loggedOutIndicator?: string;
190
+ }): Promise<LoginResult>;
191
+ /**
192
+ * Check if the current session is still alive.
193
+ *
194
+ * Used during long-running scans to detect session expiry
195
+ * and trigger re-authentication.
196
+ */
197
+ declare function checkSessionAlive(page: Page, config: {
198
+ loggedInIndicator?: string;
199
+ loggedOutIndicator?: string;
200
+ }): Promise<boolean>;
201
+
133
202
  /**
134
203
  * @vulcn/driver-browser
135
204
  *
@@ -334,4 +403,4 @@ type BrowserStep = z.infer<typeof BrowserStepSchema>;
334
403
  */
335
404
  declare const browserDriver: VulcnDriver;
336
405
 
337
- export { BROWSER_STEP_TYPES, type BrowserConfig, BrowserRecorder, BrowserRunner, type BrowserStep, BrowserStepSchema, type BrowserStepType, checkBrowsers, configSchema, crawlAndBuildSessions, browserDriver as default, installBrowsers, launchBrowser };
406
+ export { BROWSER_STEP_TYPES, type BrowserConfig, BrowserRecorder, BrowserRunner, type BrowserStep, BrowserStepSchema, type BrowserStepType, type LoginForm, type LoginResult, checkBrowsers, checkSessionAlive, configSchema, crawlAndBuildSessions, browserDriver as default, detectLoginForm, installBrowsers, launchBrowser, performLogin };