@vulcn/engine 0.7.0 → 0.8.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.
- package/CHANGELOG.md +29 -0
- package/dist/index.cjs +370 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +246 -1
- package/dist/index.d.ts +246 -1
- package/dist/index.js +361 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/driver-manager.ts","../src/driver-types.ts","../src/plugin-manager.ts","../src/plugin-types.ts"],"sourcesContent":["/**\n * @vulcn/engine - Core security testing engine\n *\n * v0.3.0: Driver-based architecture\n *\n * The engine now provides:\n * - Driver system for different recording targets (browser, api, cli)\n * - Plugin system for payloads and detection\n * - Generic session format\n *\n * Drivers handle:\n * - Recording interactions (RecorderDriver)\n * - Replaying with payload injection (RunnerDriver)\n *\n * Plugins handle:\n * - Payload loading (builtin, payloadbox, custom files)\n * - Vulnerability detection (reflection, execution, etc.)\n * - Reporting (JSON, SARIF, HTML)\n */\n\n// ============================================================================\n// Driver System\n// ============================================================================\n\nexport { DriverManager, driverManager } from \"./driver-manager\";\nexport { DRIVER_API_VERSION } from \"./driver-types\";\nexport type {\n VulcnDriver,\n RecorderDriver,\n RunnerDriver,\n RecordingHandle,\n RecordOptions,\n CrawlOptions,\n RunOptions,\n RunResult,\n RunContext,\n Session,\n Step,\n DriverLogger,\n LoadedDriver,\n DriverSource,\n} from \"./driver-types\";\n\n// ============================================================================\n// Plugin System\n// ============================================================================\n\nexport { PluginManager, pluginManager } from \"./plugin-manager\";\nexport { PLUGIN_API_VERSION } from \"./plugin-types\";\nexport type {\n VulcnPlugin,\n VulcnConfig,\n PluginConfig,\n PluginHooks,\n PluginContext,\n RecordContext,\n RunContext as PluginRunContext,\n DetectContext,\n LoadedPlugin as LoadedPluginInfo,\n PluginLogger,\n EngineInfo,\n PluginSource,\n} from \"./plugin-types\";\n\n// ============================================================================\n// Payload Types\n// ============================================================================\n\nexport type {\n PayloadCategory,\n PayloadSource,\n RuntimePayload,\n CustomPayload,\n CustomPayloadFile,\n} from \"./payload-types\";\n\n// ============================================================================\n// Core Types\n// ============================================================================\n\nexport type { Finding } from \"./types\";\n","/**\n * Vulcn Driver Manager\n *\n * Handles driver loading, registration, and lifecycle.\n * Drivers are loaded from npm packages or local files.\n */\n\nimport { isAbsolute, resolve } from \"node:path\";\nimport { parse, stringify } from \"yaml\";\nimport type {\n VulcnDriver,\n LoadedDriver,\n DriverSource,\n Session,\n Step,\n RunContext,\n RunResult,\n RunOptions,\n RecordOptions,\n CrawlOptions,\n RecordingHandle,\n DriverLogger,\n DRIVER_API_VERSION,\n} from \"./driver-types\";\nimport type { PluginManager } from \"./plugin-manager\";\nimport type { Finding } from \"./types\";\nimport type { RuntimePayload } from \"./payload-types\";\n\n/**\n * Driver Manager - loads and manages recording/running drivers\n */\nexport class DriverManager {\n private drivers: Map<string, LoadedDriver> = new Map();\n private defaultDriver: string | null = null;\n\n /**\n * Register a driver\n */\n register(driver: VulcnDriver, source: DriverSource = \"builtin\"): void {\n this.validateDriver(driver);\n this.drivers.set(driver.name, { driver, source });\n\n // First registered driver becomes default\n if (this.drivers.size === 1) {\n this.defaultDriver = driver.name;\n }\n }\n\n /**\n * Load a driver from npm or local path\n */\n async load(nameOrPath: string): Promise<void> {\n let driver: VulcnDriver;\n let source: DriverSource;\n\n if (\n nameOrPath.startsWith(\"./\") ||\n nameOrPath.startsWith(\"../\") ||\n isAbsolute(nameOrPath)\n ) {\n // Local file\n const resolved = isAbsolute(nameOrPath)\n ? nameOrPath\n : resolve(process.cwd(), nameOrPath);\n const module = await import(resolved);\n driver = module.default || module;\n source = \"local\";\n } else {\n // npm package\n const module = await import(nameOrPath);\n driver = module.default || module;\n source = \"npm\";\n }\n\n this.register(driver, source);\n }\n\n /**\n * Get a loaded driver by name\n */\n get(name: string): VulcnDriver | undefined {\n return this.drivers.get(name)?.driver;\n }\n\n /**\n * Get the default driver\n */\n getDefault(): VulcnDriver | undefined {\n if (!this.defaultDriver) return undefined;\n return this.get(this.defaultDriver);\n }\n\n /**\n * Set the default driver\n */\n setDefault(name: string): void {\n if (!this.drivers.has(name)) {\n throw new Error(`Driver \"${name}\" is not registered`);\n }\n this.defaultDriver = name;\n }\n\n /**\n * Check if a driver is registered\n */\n has(name: string): boolean {\n return this.drivers.has(name);\n }\n\n /**\n * Get all registered drivers\n */\n list(): LoadedDriver[] {\n return Array.from(this.drivers.values());\n }\n\n /**\n * Get driver for a session\n */\n getForSession(session: Session): VulcnDriver {\n const driverName = session.driver;\n const driver = this.get(driverName);\n\n if (!driver) {\n throw new Error(\n `Driver \"${driverName}\" not found. Install @vulcn/driver-${driverName} or load it manually.`,\n );\n }\n\n return driver;\n }\n\n /**\n * Parse a YAML session string into a Session object.\n *\n * Handles both new driver-format sessions and legacy v1 sessions.\n * Legacy sessions (those with non-namespaced step types like \"click\",\n * \"input\", \"navigate\") are automatically converted to the driver format\n * (e.g., \"browser.click\", \"browser.input\", \"browser.navigate\").\n *\n * @param yaml - Raw YAML string\n * @param defaultDriver - Driver to assign for legacy sessions (default: \"browser\")\n */\n parseSession(yaml: string, defaultDriver = \"browser\"): Session {\n const data = parse(yaml) as Record<string, unknown>;\n\n // Already in driver format — has a `driver` field\n if (data.driver && typeof data.driver === \"string\") {\n return data as unknown as Session;\n }\n\n // Legacy format — convert to driver session\n const steps = (data.steps as Array<Record<string, unknown>>) ?? [];\n const convertedSteps: Step[] = steps.map((step) => {\n const type = step.type as string;\n\n // If step type is already namespaced (e.g. \"browser.click\"), keep it\n if (type.includes(\".\")) {\n return step as unknown as Step;\n }\n\n // Convert legacy type → namespaced type\n return {\n ...step,\n type: `${defaultDriver}.${type}`,\n } as unknown as Step;\n });\n\n return {\n name: (data.name as string) ?? \"Untitled Session\",\n driver: defaultDriver,\n driverConfig: {\n browser: data.browser ?? \"chromium\",\n viewport: data.viewport ?? { width: 1280, height: 720 },\n startUrl: data.startUrl as string,\n },\n steps: convertedSteps,\n metadata: {\n recordedAt: data.recordedAt as string,\n version: (data.version as string) ?? \"1\",\n },\n };\n }\n\n /**\n * Start recording with a driver\n */\n async startRecording(\n driverName: string,\n config: Record<string, unknown>,\n options: RecordOptions = {},\n ): Promise<RecordingHandle> {\n const driver = this.get(driverName);\n\n if (!driver) {\n throw new Error(`Driver \"${driverName}\" not found`);\n }\n\n return driver.recorder.start(config, options);\n }\n\n /**\n * Auto-crawl a URL using a driver.\n *\n * Uses the driver's optional crawl() method to automatically\n * discover forms and injection points, returning Session[] that\n * can be passed to execute().\n *\n * Not all drivers support this — only browser has crawl capability.\n * CLI and API drivers will throw.\n */\n async crawl(\n driverName: string,\n config: Record<string, unknown>,\n options: CrawlOptions = {},\n ): Promise<Session[]> {\n const driver = this.get(driverName);\n\n if (!driver) {\n throw new Error(`Driver \"${driverName}\" not found`);\n }\n\n if (!driver.recorder.crawl) {\n throw new Error(\n `Driver \"${driverName}\" does not support auto-crawl. Use manual recording instead.`,\n );\n }\n\n return driver.recorder.crawl(config, options);\n }\n\n /**\n * Execute a session\n * Invokes plugin hooks (onRunStart, onRunEnd) around the driver runner.\n * Plugin onRunStart is deferred until the driver signals the page is ready\n * via the onPageReady callback, ensuring plugins get a real page object.\n */\n async execute(\n session: Session,\n pluginManager: PluginManager,\n options: RunOptions = {},\n ): Promise<RunResult> {\n const driver = this.getForSession(session);\n const findings: Finding[] = [];\n const logger = this.createLogger(driver.name);\n\n // Shared addFinding function — used by both internal RunContext and\n // plugin context. Ensures all findings (active + passive) flow through\n // the onFinding callback so consumers get notified consistently.\n const addFinding = (finding: Finding) => {\n findings.push(finding);\n pluginManager.addFinding(finding);\n options.onFinding?.(finding);\n };\n\n // Build a plugin context template for hooks (page is set in onPageReady)\n const pluginCtx = {\n session,\n page: null as unknown,\n headless: !!(options as Record<string, unknown>).headless,\n config: {} as Record<string, unknown>,\n engine: { version: \"0.3.0\", pluginApiVersion: 1 },\n payloads: pluginManager.getPayloads(),\n findings,\n addFinding,\n logger,\n fetch: globalThis.fetch,\n };\n\n const ctx: RunContext = {\n session,\n pluginManager,\n payloads: pluginManager.getPayloads(),\n findings,\n addFinding,\n logger,\n options: {\n ...options,\n // Provide onPageReady callback — fires plugin onRunStart hooks\n // with the real page object once the driver has created it\n onPageReady: async (page: unknown) => {\n pluginCtx.page = page;\n\n for (const loaded of pluginManager.getPlugins()) {\n if (loaded.enabled && loaded.plugin.hooks?.onRunStart) {\n try {\n await loaded.plugin.hooks.onRunStart({\n ...pluginCtx,\n config: loaded.config,\n });\n } catch (err) {\n logger.warn(\n `Plugin ${loaded.plugin.name} onRunStart failed: ${err}`,\n );\n }\n }\n }\n },\n // Fires before browser closes — lets plugins flush pending async work\n onBeforeClose: async (_page: unknown) => {\n for (const loaded of pluginManager.getPlugins()) {\n if (loaded.enabled && loaded.plugin.hooks?.onBeforeClose) {\n try {\n await loaded.plugin.hooks.onBeforeClose({\n ...pluginCtx,\n config: loaded.config,\n });\n } catch (err) {\n logger.warn(\n `Plugin ${loaded.plugin.name} onBeforeClose failed: ${err}`,\n );\n }\n }\n }\n },\n },\n };\n\n // Execute via driver runner\n // (runner calls ctx.options.onPageReady(page) after creating the page)\n let result = await driver.runner.execute(session, ctx);\n\n // Call onRunEnd hooks (e.g., report generation)\n for (const loaded of pluginManager.getPlugins()) {\n if (loaded.enabled && loaded.plugin.hooks?.onRunEnd) {\n try {\n result = await loaded.plugin.hooks.onRunEnd(result, {\n ...pluginCtx,\n config: loaded.config,\n findings: result.findings,\n });\n } catch (err) {\n logger.warn(`Plugin ${loaded.plugin.name} onRunEnd failed: ${err}`);\n }\n }\n }\n\n return result;\n }\n\n /**\n * Validate driver structure\n */\n private validateDriver(driver: unknown): asserts driver is VulcnDriver {\n if (!driver || typeof driver !== \"object\") {\n throw new Error(\"Driver must be an object\");\n }\n\n const d = driver as Record<string, unknown>;\n\n if (typeof d.name !== \"string\" || !d.name) {\n throw new Error(\"Driver must have a name\");\n }\n\n if (typeof d.version !== \"string\" || !d.version) {\n throw new Error(\"Driver must have a version\");\n }\n\n if (!Array.isArray(d.stepTypes) || d.stepTypes.length === 0) {\n throw new Error(\"Driver must define stepTypes\");\n }\n\n if (!d.recorder || typeof d.recorder !== \"object\") {\n throw new Error(\"Driver must have a recorder\");\n }\n\n if (!d.runner || typeof d.runner !== \"object\") {\n throw new Error(\"Driver must have a runner\");\n }\n }\n\n /**\n * Create a scoped logger for a driver\n */\n private createLogger(name: string): DriverLogger {\n const prefix = `[driver:${name}]`;\n return {\n debug: (msg, ...args) => console.debug(prefix, msg, ...args),\n info: (msg, ...args) => console.info(prefix, msg, ...args),\n warn: (msg, ...args) => console.warn(prefix, msg, ...args),\n error: (msg, ...args) => console.error(prefix, msg, ...args),\n };\n }\n}\n\n/**\n * Default driver manager instance\n */\nexport const driverManager = new DriverManager();\n","/**\n * Vulcn Driver System\n *\n * Drivers handle recording and running sessions for different targets:\n * - browser: Web applications (Playwright)\n * - api: REST/HTTP APIs\n * - cli: Command-line tools\n *\n * Each driver implements RecorderDriver and RunnerDriver interfaces.\n */\n\nimport type { z } from \"zod\";\nimport type { Finding } from \"./types\";\nimport type { RuntimePayload } from \"./payload-types\";\nimport type { PluginManager } from \"./plugin-manager\";\n\n/**\n * Current driver API version\n */\nexport const DRIVER_API_VERSION = 1;\n\n/**\n * Generic step - drivers define their own step types\n */\nexport interface Step {\n /** Unique step ID */\n id: string;\n\n /** Step type (namespaced, e.g., \"browser.click\", \"api.request\") */\n type: string;\n\n /** Timestamp when step was recorded */\n timestamp: number;\n\n /** Step-specific data */\n [key: string]: unknown;\n}\n\n/**\n * Generic session format\n */\nexport interface Session {\n /** Session name */\n name: string;\n\n /** Driver that recorded this session */\n driver: string;\n\n /** Driver-specific configuration */\n driverConfig: Record<string, unknown>;\n\n /** Recorded steps */\n steps: Step[];\n\n /** Session metadata */\n metadata?: {\n recordedAt?: string;\n version?: string;\n [key: string]: unknown;\n };\n}\n\n/**\n * Recording context passed to drivers\n */\nexport interface RecordContext {\n /** Session being built */\n session: Partial<Session>;\n\n /** Add a step to the session */\n addStep(step: Omit<Step, \"id\" | \"timestamp\">): void;\n\n /** Logger */\n logger: DriverLogger;\n}\n\n/**\n * Running context passed to drivers\n */\nexport interface RunContext {\n /** Session being executed */\n session: Session;\n\n /** Plugin manager for calling hooks */\n pluginManager: PluginManager;\n\n /** Available payloads */\n payloads: RuntimePayload[];\n\n /** Collected findings */\n findings: Finding[];\n\n /** Add a finding */\n addFinding(finding: Finding): void;\n\n /** Logger */\n logger: DriverLogger;\n\n /** Running options */\n options: RunOptions;\n}\n\n/**\n * Options for recording\n */\nexport interface RecordOptions {\n /** Enable auto-crawl mode (driver discovers forms automatically) */\n auto?: boolean;\n\n /** Crawl options (only used when auto=true) */\n crawlOptions?: CrawlOptions;\n\n /** Driver-specific options */\n [key: string]: unknown;\n}\n\n/**\n * Options for auto-crawl mode\n *\n * When a driver supports crawling, these options control how\n * the automated discovery works. Not all drivers support crawling —\n * it's optional and primarily used by the browser driver.\n */\nexport interface CrawlOptions {\n /** Maximum crawl depth (0 = only the given URL, default: 2) */\n maxDepth?: number;\n\n /** Maximum number of pages to visit (default: 20) */\n maxPages?: number;\n\n /** Timeout per page navigation in ms (default: 10000) */\n pageTimeout?: number;\n\n /** Only crawl pages under the same origin (default: true) */\n sameOrigin?: boolean;\n\n /** Callback when a page is crawled */\n onPageCrawled?: (url: string, formsFound: number) => void;\n}\n\n/**\n * Options for running\n */\nexport interface RunOptions {\n /** Run headless (for visual drivers) */\n headless?: boolean;\n\n /** Callback for findings */\n onFinding?: (finding: Finding) => void;\n\n /** Callback for step completion */\n onStepComplete?: (stepId: string, payloadCount: number) => void;\n\n /**\n * Called by the driver runner after the page/environment is ready.\n * The driver-manager uses this to fire plugin onRunStart hooks\n * with the real page object (instead of null).\n */\n onPageReady?: (page: unknown) => Promise<void>;\n\n /**\n * Called by the driver runner before closing the browser/environment.\n * The driver-manager uses this to fire plugin onBeforeClose hooks\n * so plugins can flush pending async work.\n */\n onBeforeClose?: (page: unknown) => Promise<void>;\n\n /** Driver-specific options */\n [key: string]: unknown;\n}\n\n/**\n * Run result\n */\nexport interface RunResult {\n /** All findings */\n findings: Finding[];\n\n /** Steps executed */\n stepsExecuted: number;\n\n /** Payloads tested */\n payloadsTested: number;\n\n /** Duration in milliseconds */\n duration: number;\n\n /** Errors encountered */\n errors: string[];\n}\n\n/**\n * Driver logger\n */\nexport interface DriverLogger {\n debug(msg: string, ...args: unknown[]): void;\n info(msg: string, ...args: unknown[]): void;\n warn(msg: string, ...args: unknown[]): void;\n error(msg: string, ...args: unknown[]): void;\n}\n\n/**\n * Recorder Driver Interface\n *\n * Implement this to add recording support for a target type.\n */\nexport interface RecorderDriver {\n /** Start recording and return control handle */\n start(\n config: Record<string, unknown>,\n options: RecordOptions,\n ): Promise<RecordingHandle>;\n\n /**\n * Auto-crawl a URL and generate sessions.\n *\n * Optional — only drivers that support automated discovery\n * (e.g., browser) implement this. CLI and API drivers do not.\n *\n * When options.auto=true is passed to startRecording, the engine\n * calls this instead of start().\n */\n crawl?(\n config: Record<string, unknown>,\n options: CrawlOptions,\n ): Promise<Session[]>;\n}\n\n/**\n * Handle returned by RecorderDriver.start()\n */\nexport interface RecordingHandle {\n /** Stop recording and return the session */\n stop(): Promise<Session>;\n\n /** Abort recording without saving */\n abort(): Promise<void>;\n\n /** Get current steps (during recording) */\n getSteps(): Step[];\n\n /** Manually add a step */\n addStep(step: Omit<Step, \"id\" | \"timestamp\">): void;\n}\n\n/**\n * Runner Driver Interface\n *\n * Implement this to add running/replay support for a target type.\n */\nexport interface RunnerDriver {\n /** Execute a session with payloads */\n execute(session: Session, ctx: RunContext): Promise<RunResult>;\n}\n\n/**\n * Complete driver definition\n */\nexport interface VulcnDriver {\n /** Unique driver name (e.g., \"browser\", \"api\", \"cli\") */\n name: string;\n\n /** Driver version */\n version: string;\n\n /** Driver API version */\n apiVersion?: number;\n\n /** Human-readable description */\n description?: string;\n\n /** Configuration schema (Zod) */\n configSchema?: z.ZodSchema;\n\n /** Step types this driver handles */\n stepTypes: string[];\n\n /** Recorder implementation */\n recorder: RecorderDriver;\n\n /** Runner implementation */\n runner: RunnerDriver;\n}\n\n/**\n * Driver source for loading\n */\nexport type DriverSource = \"npm\" | \"local\" | \"builtin\";\n\n/**\n * Loaded driver with metadata\n */\nexport interface LoadedDriver {\n driver: VulcnDriver;\n source: DriverSource;\n}\n","/**\n * Vulcn Plugin Manager\n * Handles plugin loading, lifecycle, and hook execution\n */\n\nimport { readFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { resolve, isAbsolute } from \"node:path\";\nimport YAML from \"yaml\";\nimport { z } from \"zod\";\nimport type {\n VulcnPlugin,\n VulcnConfig,\n PluginConfig,\n LoadedPlugin,\n PluginContext,\n PluginSource,\n PluginLogger,\n EngineInfo,\n PluginHooks,\n} from \"./plugin-types\";\nimport { PLUGIN_API_VERSION } from \"./plugin-types\";\nimport type { Finding } from \"./types\";\nimport type { RuntimePayload } from \"./payload-types\";\n\n// Package version (injected at build time or read from package.json)\nconst ENGINE_VERSION = \"0.2.0\";\n\n/**\n * Config file schema\n */\nconst VulcnConfigSchema = z.object({\n version: z.string().default(\"1\"),\n plugins: z\n .array(\n z.object({\n name: z.string(),\n config: z.record(z.unknown()).optional(),\n enabled: z.boolean().default(true),\n }),\n )\n .optional(),\n settings: z\n .object({\n browser: z.enum([\"chromium\", \"firefox\", \"webkit\"]).optional(),\n headless: z.boolean().optional(),\n timeout: z.number().optional(),\n })\n .optional(),\n});\n\n/**\n * Plugin Manager - loads, configures, and orchestrates plugins\n */\nexport class PluginManager {\n private plugins: LoadedPlugin[] = [];\n private config: VulcnConfig | null = null;\n private initialized = false;\n\n /**\n * Shared context passed to all plugins\n */\n private sharedPayloads: RuntimePayload[] = [];\n private sharedFindings: Finding[] = [];\n\n /**\n * Load configuration from vulcn.config.yml\n */\n async loadConfig(configPath?: string): Promise<VulcnConfig> {\n const paths = configPath\n ? [configPath]\n : [\n \"vulcn.config.yml\",\n \"vulcn.config.yaml\",\n \"vulcn.config.json\",\n \".vulcnrc.yml\",\n \".vulcnrc.yaml\",\n \".vulcnrc.json\",\n ];\n\n for (const path of paths) {\n const resolved = isAbsolute(path) ? path : resolve(process.cwd(), path);\n if (existsSync(resolved)) {\n const content = await readFile(resolved, \"utf-8\");\n const parsed = path.endsWith(\".json\")\n ? JSON.parse(content)\n : YAML.parse(content);\n this.config = VulcnConfigSchema.parse(parsed);\n return this.config;\n }\n }\n\n // No config file - use defaults\n this.config = { version: \"1\", plugins: [], settings: {} };\n return this.config;\n }\n\n /**\n * Load all plugins from config\n */\n async loadPlugins(): Promise<void> {\n if (!this.config) {\n await this.loadConfig();\n }\n\n const pluginConfigs = this.config?.plugins || [];\n\n for (const pluginConfig of pluginConfigs) {\n if (pluginConfig.enabled === false) continue;\n\n try {\n const loaded = await this.loadPlugin(pluginConfig);\n this.plugins.push(loaded);\n } catch (err) {\n console.error(\n `Failed to load plugin ${pluginConfig.name}:`,\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n }\n\n /**\n * Load a single plugin\n */\n private async loadPlugin(config: PluginConfig): Promise<LoadedPlugin> {\n const { name, config: pluginConfig = {} } = config;\n let plugin: VulcnPlugin;\n let source: PluginSource;\n\n // Determine plugin source and load\n if (name.startsWith(\"./\") || name.startsWith(\"../\") || isAbsolute(name)) {\n // Local file plugin\n const resolved = isAbsolute(name) ? name : resolve(process.cwd(), name);\n const module = await import(resolved);\n plugin = module.default || module;\n source = \"local\";\n } else if (name.startsWith(\"@vulcn/\")) {\n // Official plugin (npm package)\n const module = await import(name);\n plugin = module.default || module;\n source = \"npm\";\n } else {\n // Community plugin (npm package)\n const module = await import(name);\n plugin = module.default || module;\n source = \"npm\";\n }\n\n // Validate plugin structure\n this.validatePlugin(plugin);\n\n // Validate plugin config if schema provided\n let resolvedConfig = pluginConfig;\n if (plugin.configSchema) {\n try {\n resolvedConfig = plugin.configSchema.parse(pluginConfig);\n } catch (err) {\n throw new Error(\n `Invalid config for plugin ${name}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n return {\n plugin,\n config: resolvedConfig,\n source,\n enabled: true,\n };\n }\n\n /**\n * Validate plugin structure\n */\n private validatePlugin(plugin: unknown): asserts plugin is VulcnPlugin {\n if (!plugin || typeof plugin !== \"object\") {\n throw new Error(\"Plugin must be an object\");\n }\n\n const p = plugin as Record<string, unknown>;\n if (typeof p.name !== \"string\" || !p.name) {\n throw new Error(\"Plugin must have a name\");\n }\n if (typeof p.version !== \"string\" || !p.version) {\n throw new Error(\"Plugin must have a version\");\n }\n\n // Check API version compatibility\n const apiVersion = (p.apiVersion as number) || 1;\n if (apiVersion > PLUGIN_API_VERSION) {\n throw new Error(\n `Plugin requires API version ${apiVersion}, but engine supports ${PLUGIN_API_VERSION}`,\n );\n }\n }\n\n /**\n * Add a plugin programmatically (for testing or dynamic loading)\n */\n addPlugin(plugin: VulcnPlugin, config: Record<string, unknown> = {}): void {\n this.validatePlugin(plugin);\n this.plugins.push({\n plugin,\n config,\n source: \"custom\",\n enabled: true,\n });\n }\n\n /**\n * Initialize all plugins (call onInit hooks)\n */\n async initialize(): Promise<void> {\n if (this.initialized) return;\n\n // Load payloads from plugins that provide them\n for (const loaded of this.plugins) {\n if (loaded.plugin.payloads) {\n const payloads =\n typeof loaded.plugin.payloads === \"function\"\n ? await loaded.plugin.payloads()\n : loaded.plugin.payloads;\n this.sharedPayloads.push(...payloads);\n }\n }\n\n // Call onInit hooks\n await this.callHook(\"onInit\", (hook, ctx) => hook(ctx));\n\n this.initialized = true;\n }\n\n /**\n * Destroy all plugins (call onDestroy hooks)\n */\n async destroy(): Promise<void> {\n await this.callHook(\"onDestroy\", (hook, ctx) => hook(ctx));\n this.plugins = [];\n this.sharedPayloads = [];\n this.sharedFindings = [];\n this.initialized = false;\n }\n\n /**\n * Get all loaded payloads\n */\n getPayloads(): RuntimePayload[] {\n return this.sharedPayloads;\n }\n\n /**\n * Get all collected findings\n */\n getFindings(): Finding[] {\n return this.sharedFindings;\n }\n\n /**\n * Add a finding (used by detectors)\n */\n addFinding(finding: Finding): void {\n this.sharedFindings.push(finding);\n }\n\n /**\n * Add payloads (used by loaders)\n */\n addPayloads(payloads: RuntimePayload[]): void {\n this.sharedPayloads.push(...payloads);\n }\n\n /**\n * Clear findings (for new run)\n */\n clearFindings(): void {\n this.sharedFindings = [];\n }\n\n /**\n * Get loaded plugins\n */\n getPlugins(): LoadedPlugin[] {\n return this.plugins;\n }\n\n /**\n * Check if a plugin is loaded by name\n */\n hasPlugin(name: string): boolean {\n return this.plugins.some((p) => p.plugin.name === name);\n }\n\n /**\n * Create base context for plugins\n */\n createContext(pluginConfig: Record<string, unknown>): PluginContext {\n const engineInfo: EngineInfo = {\n version: ENGINE_VERSION,\n pluginApiVersion: PLUGIN_API_VERSION,\n };\n\n return {\n config: pluginConfig,\n engine: engineInfo,\n payloads: this.sharedPayloads,\n findings: this.sharedFindings,\n addFinding: (finding: Finding) => {\n this.sharedFindings.push(finding);\n },\n logger: this.createLogger(\"plugin\"),\n fetch: globalThis.fetch,\n };\n }\n\n /**\n * Create scoped logger for a plugin\n */\n private createLogger(name: string): PluginLogger {\n const prefix = `[${name}]`;\n return {\n debug: (msg, ...args) => console.debug(prefix, msg, ...args),\n info: (msg, ...args) => console.info(prefix, msg, ...args),\n warn: (msg, ...args) => console.warn(prefix, msg, ...args),\n error: (msg, ...args) => console.error(prefix, msg, ...args),\n };\n }\n\n /**\n * Call a hook on all plugins sequentially\n */\n async callHook<K extends keyof PluginHooks>(\n hookName: K,\n executor: (\n hook: NonNullable<PluginHooks[K]>,\n ctx: PluginContext,\n ) => Promise<unknown>,\n ): Promise<void> {\n for (const loaded of this.plugins) {\n const hook = loaded.plugin.hooks?.[hookName];\n if (hook) {\n const ctx = this.createContext(loaded.config);\n ctx.logger = this.createLogger(loaded.plugin.name);\n try {\n await executor(hook as NonNullable<PluginHooks[K]>, ctx);\n } catch (err) {\n console.error(\n `Error in plugin ${loaded.plugin.name}.${hookName}:`,\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n }\n }\n\n /**\n * Call a hook and collect results\n */\n async callHookCollect<K extends keyof PluginHooks, R>(\n hookName: K,\n executor: (\n hook: NonNullable<PluginHooks[K]>,\n ctx: PluginContext,\n ) => Promise<R | R[] | null>,\n ): Promise<R[]> {\n const results: R[] = [];\n\n for (const loaded of this.plugins) {\n const hook = loaded.plugin.hooks?.[hookName];\n if (hook) {\n const ctx = this.createContext(loaded.config);\n ctx.logger = this.createLogger(loaded.plugin.name);\n try {\n const result = await executor(\n hook as NonNullable<PluginHooks[K]>,\n ctx,\n );\n if (result !== null && result !== undefined) {\n if (Array.isArray(result)) {\n results.push(...result);\n } else {\n results.push(result);\n }\n }\n } catch (err) {\n console.error(\n `Error in plugin ${loaded.plugin.name}.${hookName}:`,\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n }\n\n return results;\n }\n\n /**\n * Call a hook that transforms a value through the pipeline\n */\n async callHookPipe<T>(\n hookName: keyof PluginHooks,\n initial: T,\n executor: (\n hook: NonNullable<PluginHooks[typeof hookName]>,\n value: T,\n ctx: PluginContext,\n ) => Promise<T>,\n ): Promise<T> {\n let value = initial;\n\n for (const loaded of this.plugins) {\n const hook = loaded.plugin.hooks?.[hookName];\n if (hook) {\n const ctx = this.createContext(loaded.config);\n ctx.logger = this.createLogger(loaded.plugin.name);\n try {\n value = await executor(\n hook as NonNullable<PluginHooks[typeof hookName]>,\n value,\n ctx,\n );\n } catch (err) {\n console.error(\n `Error in plugin ${loaded.plugin.name}.${hookName}:`,\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n }\n\n return value;\n }\n}\n\n/**\n * Default shared plugin manager instance\n */\nexport const pluginManager = new PluginManager();\n","/**\n * Vulcn Plugin System Types\n * @module @vulcn/engine/plugin\n *\n * The plugin system is driver-agnostic. Detection plugins receive\n * a generic page interface rather than Playwright types directly.\n * This allows the same plugin to work across different driver types.\n */\n\nimport type { z } from \"zod\";\nimport type { Session, Step } from \"./driver-types\";\nimport type { Finding } from \"./types\";\nimport type { RunResult } from \"./driver-types\";\nimport type { RuntimePayload, PayloadCategory } from \"./payload-types\";\n\n// Re-export for plugin authors\nexport type {\n Session,\n Step,\n Finding,\n RunResult,\n RuntimePayload,\n PayloadCategory,\n};\n\n/**\n * Plugin API version - plugins declare compatibility\n */\nexport const PLUGIN_API_VERSION = 1;\n\n/**\n * Plugin source types for identification\n */\nexport type PluginSource = \"builtin\" | \"npm\" | \"local\" | \"custom\";\n\n/**\n * Main plugin interface\n */\nexport interface VulcnPlugin {\n /** Unique plugin name (e.g., \"@vulcn/plugin-payloads\") */\n name: string;\n\n /** Plugin version (semver) */\n version: string;\n\n /** Plugin API version this plugin targets */\n apiVersion?: number;\n\n /** Human-readable description */\n description?: string;\n\n /** Lifecycle hooks */\n hooks?: PluginHooks;\n\n /**\n * Payloads provided by this plugin (Loaders)\n * Can be static array or async function for lazy loading\n */\n payloads?: RuntimePayload[] | (() => Promise<RuntimePayload[]>);\n\n /**\n * Zod schema for plugin configuration validation\n */\n configSchema?: z.ZodSchema;\n}\n\n/**\n * Plugin lifecycle hooks\n *\n * Detection hooks (onDialog, onConsoleMessage, etc.) receive\n * Playwright types from the driver. Plugins that use these\n * should declare playwright as a peer/dev dependency.\n */\nexport interface PluginHooks {\n // ─────────────────────────────────────────────────────────────────\n // Initialization\n // ─────────────────────────────────────────────────────────────────\n\n /**\n * Called when plugin is loaded, before any operation\n * Use for setup, loading payloads, etc.\n */\n onInit?: (ctx: PluginContext) => Promise<void>;\n\n /**\n * Called when plugin is unloaded/cleanup\n */\n onDestroy?: (ctx: PluginContext) => Promise<void>;\n\n // ─────────────────────────────────────────────────────────────────\n // Recording Phase\n // ─────────────────────────────────────────────────────────────────\n\n /** Called when recording starts */\n onRecordStart?: (ctx: RecordContext) => Promise<void>;\n\n /** Called for each recorded step, can transform */\n onRecordStep?: (step: Step, ctx: RecordContext) => Promise<Step>;\n\n /** Called when recording ends, can transform session */\n onRecordEnd?: (session: Session, ctx: RecordContext) => Promise<Session>;\n\n // ─────────────────────────────────────────────────────────────────\n // Running Phase\n // ─────────────────────────────────────────────────────────────────\n\n /** Called when run starts */\n onRunStart?: (ctx: RunContext) => Promise<void>;\n\n /** Called before each payload is injected, can transform payload */\n onBeforePayload?: (\n payload: string,\n step: Step,\n ctx: RunContext,\n ) => Promise<string>;\n\n /** Called after payload injection, for detection */\n onAfterPayload?: (ctx: DetectContext) => Promise<Finding[]>;\n\n /**\n * Called before the browser/driver is closed.\n * Plugins should await any pending async work here (e.g., flush\n * in-flight response handlers that need browser access).\n */\n onBeforeClose?: (ctx: PluginContext) => Promise<void>;\n\n /** Called when run ends, can transform results */\n onRunEnd?: (result: RunResult, ctx: RunContext) => Promise<RunResult>;\n\n // ─────────────────────────────────────────────────────────────────\n // Browser Event Hooks (Detection)\n // These receive driver-specific types (e.g. Playwright's Dialog)\n // ─────────────────────────────────────────────────────────────────\n\n /** Called when JavaScript alert/confirm/prompt appears */\n onDialog?: (dialog: unknown, ctx: DetectContext) => Promise<Finding | null>;\n\n /** Called on console.log/warn/error */\n onConsoleMessage?: (\n msg: unknown,\n ctx: DetectContext,\n ) => Promise<Finding | null>;\n\n /** Called on page load/navigation */\n onPageLoad?: (page: unknown, ctx: DetectContext) => Promise<Finding[]>;\n\n /** Called on network request */\n onNetworkRequest?: (\n request: unknown,\n ctx: DetectContext,\n ) => Promise<Finding | null>;\n\n /** Called on network response */\n onNetworkResponse?: (\n response: unknown,\n ctx: DetectContext,\n ) => Promise<Finding | null>;\n}\n\n/**\n * Logger interface for plugins\n */\nexport interface PluginLogger {\n debug: (msg: string, ...args: unknown[]) => void;\n info: (msg: string, ...args: unknown[]) => void;\n warn: (msg: string, ...args: unknown[]) => void;\n error: (msg: string, ...args: unknown[]) => void;\n}\n\n/**\n * Engine information exposed to plugins\n */\nexport interface EngineInfo {\n version: string;\n pluginApiVersion: number;\n}\n\n/**\n * Base context available to all plugin hooks\n */\nexport interface PluginContext {\n /** Plugin-specific config from vulcn.config.yml */\n config: Record<string, unknown>;\n\n /** Engine information */\n engine: EngineInfo;\n\n /** Shared payload registry - loaders add payloads here */\n payloads: RuntimePayload[];\n\n /** Shared findings collection (read-only view, use addFinding to add) */\n findings: Finding[];\n\n /**\n * Add a finding through the proper callback chain.\n * Plugins should use this instead of pushing to findings[] directly,\n * so consumers (CLI, worker) get notified via onFinding callbacks.\n */\n addFinding: (finding: Finding) => void;\n\n /** Scoped logger */\n logger: PluginLogger;\n\n /** Fetch API for network requests */\n fetch: typeof fetch;\n}\n\n/**\n * Context for recording phase hooks\n */\nexport interface RecordContext extends PluginContext {\n /** Page interface (driver-specific, e.g. Playwright Page) */\n page: unknown;\n}\n\n/**\n * Context for running phase hooks\n */\nexport interface RunContext extends PluginContext {\n /** Session being executed */\n session: Session;\n\n /** Page interface (driver-specific, e.g. Playwright Page) */\n page: unknown;\n\n /** Whether running headless */\n headless: boolean;\n}\n\n/**\n * Context for detection hooks\n */\nexport interface DetectContext extends RunContext {\n /** Current step being tested */\n step: Step;\n\n /** Current payload set being tested */\n payloadSet: RuntimePayload;\n\n /** Actual payload value injected */\n payloadValue: string;\n\n /** Step ID for reporting */\n stepId: string;\n}\n\n/**\n * Plugin configuration in vulcn.config.yml\n */\nexport interface PluginConfig {\n /** Plugin name/path */\n name: string;\n\n /** Plugin-specific configuration */\n config?: Record<string, unknown>;\n\n /** Whether plugin is enabled (default: true) */\n enabled?: boolean;\n}\n\n/**\n * Vulcn configuration file schema\n */\nexport interface VulcnConfig {\n /** Config version */\n version: string;\n\n /** Plugins to load */\n plugins?: PluginConfig[];\n\n /** Global settings */\n settings?: {\n headless?: boolean;\n timeout?: number;\n };\n}\n\n/**\n * Loaded plugin instance with resolved config\n */\nexport interface LoadedPlugin {\n /** Plugin definition */\n plugin: VulcnPlugin;\n\n /** Resolved configuration */\n config: Record<string, unknown>;\n\n /** Source of the plugin */\n source: PluginSource;\n\n /** Whether plugin is enabled */\n enabled: boolean;\n}\n"],"mappings":"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","names":["module","pluginManager","import_node_path","import_yaml","YAML","module"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/driver-manager.ts","../src/driver-types.ts","../src/plugin-manager.ts","../src/plugin-types.ts","../src/auth.ts","../src/session.ts"],"sourcesContent":["/**\n * @vulcn/engine - Core security testing engine\n *\n * v0.7.0: Driver-based architecture + Auth + Session v2\n *\n * The engine now provides:\n * - Driver system for different recording targets (browser, api, cli)\n * - Plugin system for payloads and detection\n * - Generic session format\n * - Credential encryption & auth state management\n * - Session format v2 (.vulcn/ directory)\n *\n * Drivers handle:\n * - Recording interactions (RecorderDriver)\n * - Replaying with payload injection (RunnerDriver)\n *\n * Plugins handle:\n * - Payload loading (builtin, payloadbox, custom files)\n * - Vulnerability detection (reflection, execution, etc.)\n * - Reporting (JSON, SARIF, HTML)\n */\n\n// ============================================================================\n// Driver System\n// ============================================================================\n\nexport { DriverManager, driverManager } from \"./driver-manager\";\nexport { DRIVER_API_VERSION } from \"./driver-types\";\nexport type {\n VulcnDriver,\n RecorderDriver,\n RunnerDriver,\n RecordingHandle,\n RecordOptions,\n CrawlOptions,\n RunOptions,\n RunResult,\n RunContext,\n Session,\n Step,\n DriverLogger,\n LoadedDriver,\n DriverSource,\n} from \"./driver-types\";\n\n// ============================================================================\n// Plugin System\n// ============================================================================\n\nexport { PluginManager, pluginManager } from \"./plugin-manager\";\nexport { PLUGIN_API_VERSION } from \"./plugin-types\";\nexport type {\n VulcnPlugin,\n VulcnConfig,\n PluginConfig,\n PluginHooks,\n PluginContext,\n RecordContext,\n RunContext as PluginRunContext,\n ScanContext,\n DetectContext,\n LoadedPlugin as LoadedPluginInfo,\n PluginLogger,\n EngineInfo,\n PluginSource,\n} from \"./plugin-types\";\n\n// ============================================================================\n// Auth System\n// ============================================================================\n\nexport {\n encrypt,\n decrypt,\n encryptCredentials,\n decryptCredentials,\n encryptStorageState,\n decryptStorageState,\n getPassphrase,\n} from \"./auth\";\nexport type {\n FormCredentials,\n HeaderCredentials,\n Credentials,\n AuthConfig,\n} from \"./auth\";\n\n// ============================================================================\n// Session Format v2\n// ============================================================================\n\nexport {\n loadSessionDir,\n saveSessionDir,\n isSessionDir,\n looksLikeSessionDir,\n readAuthState,\n readCapturedRequests,\n} from \"./session\";\nexport type { ScanManifest, SessionRef, CapturedRequest } from \"./session\";\n\n// ============================================================================\n// Payload Types\n// ============================================================================\n\nexport type {\n PayloadCategory,\n PayloadSource,\n RuntimePayload,\n CustomPayload,\n CustomPayloadFile,\n} from \"./payload-types\";\n\n// ============================================================================\n// Core Types\n// ============================================================================\n\nexport type { Finding } from \"./types\";\n","/**\n * Vulcn Driver Manager\n *\n * Handles driver loading, registration, and lifecycle.\n * Drivers are loaded from npm packages or local files.\n */\n\nimport { isAbsolute, resolve } from \"node:path\";\nimport { parse, stringify } from \"yaml\";\nimport type {\n VulcnDriver,\n LoadedDriver,\n DriverSource,\n Session,\n Step,\n RunContext,\n RunResult,\n RunOptions,\n RecordOptions,\n CrawlOptions,\n RecordingHandle,\n DriverLogger,\n DRIVER_API_VERSION,\n} from \"./driver-types\";\nimport type { PluginManager } from \"./plugin-manager\";\nimport type { Finding } from \"./types\";\nimport type { RuntimePayload } from \"./payload-types\";\nimport type { ScanContext } from \"./plugin-types\";\n\n/**\n * Driver Manager - loads and manages recording/running drivers\n */\nexport class DriverManager {\n private drivers: Map<string, LoadedDriver> = new Map();\n private defaultDriver: string | null = null;\n\n /**\n * Register a driver\n */\n register(driver: VulcnDriver, source: DriverSource = \"builtin\"): void {\n this.validateDriver(driver);\n this.drivers.set(driver.name, { driver, source });\n\n // First registered driver becomes default\n if (this.drivers.size === 1) {\n this.defaultDriver = driver.name;\n }\n }\n\n /**\n * Load a driver from npm or local path\n */\n async load(nameOrPath: string): Promise<void> {\n let driver: VulcnDriver;\n let source: DriverSource;\n\n if (\n nameOrPath.startsWith(\"./\") ||\n nameOrPath.startsWith(\"../\") ||\n isAbsolute(nameOrPath)\n ) {\n // Local file\n const resolved = isAbsolute(nameOrPath)\n ? nameOrPath\n : resolve(process.cwd(), nameOrPath);\n const module = await import(resolved);\n driver = module.default || module;\n source = \"local\";\n } else {\n // npm package\n const module = await import(nameOrPath);\n driver = module.default || module;\n source = \"npm\";\n }\n\n this.register(driver, source);\n }\n\n /**\n * Get a loaded driver by name\n */\n get(name: string): VulcnDriver | undefined {\n return this.drivers.get(name)?.driver;\n }\n\n /**\n * Get the default driver\n */\n getDefault(): VulcnDriver | undefined {\n if (!this.defaultDriver) return undefined;\n return this.get(this.defaultDriver);\n }\n\n /**\n * Set the default driver\n */\n setDefault(name: string): void {\n if (!this.drivers.has(name)) {\n throw new Error(`Driver \"${name}\" is not registered`);\n }\n this.defaultDriver = name;\n }\n\n /**\n * Check if a driver is registered\n */\n has(name: string): boolean {\n return this.drivers.has(name);\n }\n\n /**\n * Get all registered drivers\n */\n list(): LoadedDriver[] {\n return Array.from(this.drivers.values());\n }\n\n /**\n * Get driver for a session\n */\n getForSession(session: Session): VulcnDriver {\n const driverName = session.driver;\n const driver = this.get(driverName);\n\n if (!driver) {\n throw new Error(\n `Driver \"${driverName}\" not found. Install @vulcn/driver-${driverName} or load it manually.`,\n );\n }\n\n return driver;\n }\n\n /**\n * Parse a YAML session string into a Session object.\n *\n * Handles both new driver-format sessions and legacy v1 sessions.\n * Legacy sessions (those with non-namespaced step types like \"click\",\n * \"input\", \"navigate\") are automatically converted to the driver format\n * (e.g., \"browser.click\", \"browser.input\", \"browser.navigate\").\n *\n * @param yaml - Raw YAML string\n * @param defaultDriver - Driver to assign for legacy sessions (default: \"browser\")\n */\n parseSession(yaml: string, defaultDriver = \"browser\"): Session {\n const data = parse(yaml) as Record<string, unknown>;\n\n // Already in driver format — has a `driver` field\n if (data.driver && typeof data.driver === \"string\") {\n return data as unknown as Session;\n }\n\n // Legacy format — convert to driver session\n const steps = (data.steps as Array<Record<string, unknown>>) ?? [];\n const convertedSteps: Step[] = steps.map((step) => {\n const type = step.type as string;\n\n // If step type is already namespaced (e.g. \"browser.click\"), keep it\n if (type.includes(\".\")) {\n return step as unknown as Step;\n }\n\n // Convert legacy type → namespaced type\n return {\n ...step,\n type: `${defaultDriver}.${type}`,\n } as unknown as Step;\n });\n\n return {\n name: (data.name as string) ?? \"Untitled Session\",\n driver: defaultDriver,\n driverConfig: {\n browser: data.browser ?? \"chromium\",\n viewport: data.viewport ?? { width: 1280, height: 720 },\n startUrl: data.startUrl as string,\n },\n steps: convertedSteps,\n metadata: {\n recordedAt: data.recordedAt as string,\n version: (data.version as string) ?? \"1\",\n },\n };\n }\n\n /**\n * Start recording with a driver\n */\n async startRecording(\n driverName: string,\n config: Record<string, unknown>,\n options: RecordOptions = {},\n ): Promise<RecordingHandle> {\n const driver = this.get(driverName);\n\n if (!driver) {\n throw new Error(`Driver \"${driverName}\" not found`);\n }\n\n return driver.recorder.start(config, options);\n }\n\n /**\n * Auto-crawl a URL using a driver.\n *\n * Uses the driver's optional crawl() method to automatically\n * discover forms and injection points, returning Session[] that\n * can be passed to execute().\n *\n * Not all drivers support this — only browser has crawl capability.\n * CLI and API drivers will throw.\n */\n async crawl(\n driverName: string,\n config: Record<string, unknown>,\n options: CrawlOptions = {},\n ): Promise<Session[]> {\n const driver = this.get(driverName);\n\n if (!driver) {\n throw new Error(`Driver \"${driverName}\" not found`);\n }\n\n if (!driver.recorder.crawl) {\n throw new Error(\n `Driver \"${driverName}\" does not support auto-crawl. Use manual recording instead.`,\n );\n }\n\n return driver.recorder.crawl(config, options);\n }\n\n /**\n * Execute a session\n * Invokes plugin hooks (onRunStart, onRunEnd) around the driver runner.\n * Plugin onRunStart is deferred until the driver signals the page is ready\n * via the onPageReady callback, ensuring plugins get a real page object.\n */\n async execute(\n session: Session,\n pluginManager: PluginManager,\n options: RunOptions = {},\n ): Promise<RunResult> {\n const driver = this.getForSession(session);\n const findings: Finding[] = [];\n const logger = this.createLogger(driver.name);\n\n // Shared addFinding function — used by both internal RunContext and\n // plugin context. Ensures all findings (active + passive) flow through\n // the onFinding callback so consumers get notified consistently.\n const addFinding = (finding: Finding) => {\n findings.push(finding);\n pluginManager.addFinding(finding);\n options.onFinding?.(finding);\n };\n\n // Build a plugin context template for hooks (page is set in onPageReady)\n const pluginCtx = {\n session,\n page: null as unknown,\n headless: !!(options as Record<string, unknown>).headless,\n config: {} as Record<string, unknown>,\n engine: { version: \"0.3.0\", pluginApiVersion: 1 },\n payloads: pluginManager.getPayloads(),\n findings,\n addFinding,\n logger,\n fetch: globalThis.fetch,\n };\n\n const ctx: RunContext = {\n session,\n pluginManager,\n payloads: pluginManager.getPayloads(),\n findings,\n addFinding,\n logger,\n options: {\n ...options,\n // Provide onPageReady callback — fires plugin onRunStart hooks\n // with the real page object once the driver has created it\n onPageReady: async (page: unknown) => {\n pluginCtx.page = page;\n\n for (const loaded of pluginManager.getPlugins()) {\n if (loaded.enabled && loaded.plugin.hooks?.onRunStart) {\n try {\n await loaded.plugin.hooks.onRunStart({\n ...pluginCtx,\n config: loaded.config,\n });\n } catch (err) {\n logger.warn(\n `Plugin ${loaded.plugin.name} onRunStart failed: ${err}`,\n );\n }\n }\n }\n },\n // Fires before browser closes — lets plugins flush pending async work\n onBeforeClose: async (_page: unknown) => {\n for (const loaded of pluginManager.getPlugins()) {\n if (loaded.enabled && loaded.plugin.hooks?.onBeforeClose) {\n try {\n await loaded.plugin.hooks.onBeforeClose({\n ...pluginCtx,\n config: loaded.config,\n });\n } catch (err) {\n logger.warn(\n `Plugin ${loaded.plugin.name} onBeforeClose failed: ${err}`,\n );\n }\n }\n }\n },\n },\n };\n\n // Execute via driver runner\n // (runner calls ctx.options.onPageReady(page) after creating the page)\n let result = await driver.runner.execute(session, ctx);\n\n // Call onRunEnd hooks (e.g., report generation)\n for (const loaded of pluginManager.getPlugins()) {\n if (loaded.enabled && loaded.plugin.hooks?.onRunEnd) {\n try {\n result = await loaded.plugin.hooks.onRunEnd(result, {\n ...pluginCtx,\n config: loaded.config,\n findings: result.findings,\n });\n } catch (err) {\n logger.warn(`Plugin ${loaded.plugin.name} onRunEnd failed: ${err}`);\n }\n }\n }\n\n return result;\n }\n\n /**\n * Execute multiple sessions with a shared browser (scan-level orchestration).\n *\n * This is the preferred entry point for running a full scan. It:\n * 1. Launches ONE browser for the entire scan\n * 2. Passes the browser to each session's runner via options.browser\n * 3. Each session creates its own context (lightweight, isolated cookies)\n * 4. Aggregates results across all sessions\n * 5. Closes the browser once at the end\n *\n * This is 5-10x faster than calling execute() per session because\n * launching a browser takes 2-3 seconds.\n */\n async executeScan(\n sessions: Session[],\n pluginManager: PluginManager,\n options: RunOptions = {},\n ): Promise<{\n results: RunResult[];\n aggregate: RunResult;\n }> {\n if (sessions.length === 0) {\n const empty: RunResult = {\n findings: [],\n stepsExecuted: 0,\n payloadsTested: 0,\n duration: 0,\n errors: [\"No sessions to execute\"],\n };\n return { results: [], aggregate: empty };\n }\n\n const startTime = Date.now();\n const results: RunResult[] = [];\n const allFindings: Finding[] = [];\n let totalSteps = 0;\n let totalPayloads = 0;\n const allErrors: string[] = [];\n\n // Launch shared browser via the first session's driver\n // (all sessions should use the same driver in a scan)\n const firstDriver = this.getForSession(sessions[0]);\n let sharedBrowser: unknown = null;\n\n // Only share browser for browser driver\n if (firstDriver.name === \"browser\") {\n try {\n // Dynamic import to avoid hard dependency on driver-browser\n // Use variable to bypass TS module resolution (engine doesn't depend on driver-browser)\n const driverPkg = \"@vulcn/driver-browser\";\n const { launchBrowser } = await import(/* @vite-ignore */ driverPkg);\n const browserType =\n (sessions[0].driverConfig.browser as string) ?? \"chromium\";\n const headless = options.headless ?? true;\n const result = await launchBrowser({\n browser: browserType as \"chromium\" | \"firefox\" | \"webkit\",\n headless,\n });\n sharedBrowser = result.browser;\n } catch {\n // If we can't launch a shared browser, fall back to per-session\n }\n }\n\n try {\n // Fire onScanStart hooks\n await pluginManager.callHook(\"onScanStart\", async (hook, ctx) => {\n const scanCtx: ScanContext = {\n ...ctx,\n sessions,\n headless: options.headless ?? true,\n sessionCount: sessions.length,\n };\n await (hook as (ctx: ScanContext) => Promise<void>)(scanCtx);\n });\n\n for (const session of sessions) {\n const sessionOptions: RunOptions = {\n ...options,\n ...(sharedBrowser ? { browser: sharedBrowser } : {}),\n };\n\n const result = await this.execute(\n session,\n pluginManager,\n sessionOptions,\n );\n results.push(result);\n allFindings.push(...result.findings);\n totalSteps += result.stepsExecuted;\n totalPayloads += result.payloadsTested;\n allErrors.push(...result.errors);\n }\n } finally {\n // Close the shared browser\n if (\n sharedBrowser &&\n typeof (sharedBrowser as { close: () => Promise<void> }).close ===\n \"function\"\n ) {\n await (sharedBrowser as { close: () => Promise<void> }).close();\n }\n }\n\n const aggregate: RunResult = {\n findings: allFindings,\n stepsExecuted: totalSteps,\n payloadsTested: totalPayloads,\n duration: Date.now() - startTime,\n errors: allErrors,\n };\n\n // Fire onScanEnd hooks — allows plugins to transform the aggregate result\n let finalAggregate = aggregate;\n finalAggregate = await pluginManager.callHookPipe(\n \"onScanEnd\",\n finalAggregate,\n async (hook, value, ctx) => {\n const scanCtx: ScanContext = {\n ...ctx,\n sessions,\n headless: options.headless ?? true,\n sessionCount: sessions.length,\n };\n return await (\n hook as (result: RunResult, ctx: ScanContext) => Promise<RunResult>\n )(value, scanCtx);\n },\n );\n\n return { results, aggregate: finalAggregate };\n }\n\n /**\n * Validate driver structure\n */\n private validateDriver(driver: unknown): asserts driver is VulcnDriver {\n if (!driver || typeof driver !== \"object\") {\n throw new Error(\"Driver must be an object\");\n }\n\n const d = driver as Record<string, unknown>;\n\n if (typeof d.name !== \"string\" || !d.name) {\n throw new Error(\"Driver must have a name\");\n }\n\n if (typeof d.version !== \"string\" || !d.version) {\n throw new Error(\"Driver must have a version\");\n }\n\n if (!Array.isArray(d.stepTypes) || d.stepTypes.length === 0) {\n throw new Error(\"Driver must define stepTypes\");\n }\n\n if (!d.recorder || typeof d.recorder !== \"object\") {\n throw new Error(\"Driver must have a recorder\");\n }\n\n if (!d.runner || typeof d.runner !== \"object\") {\n throw new Error(\"Driver must have a runner\");\n }\n }\n\n /**\n * Create a scoped logger for a driver\n */\n private createLogger(name: string): DriverLogger {\n const prefix = `[driver:${name}]`;\n return {\n debug: (msg, ...args) => console.debug(prefix, msg, ...args),\n info: (msg, ...args) => console.info(prefix, msg, ...args),\n warn: (msg, ...args) => console.warn(prefix, msg, ...args),\n error: (msg, ...args) => console.error(prefix, msg, ...args),\n };\n }\n}\n\n/**\n * Default driver manager instance\n */\nexport const driverManager = new DriverManager();\n","/**\n * Vulcn Driver System\n *\n * Drivers handle recording and running sessions for different targets:\n * - browser: Web applications (Playwright)\n * - api: REST/HTTP APIs\n * - cli: Command-line tools\n *\n * Each driver implements RecorderDriver and RunnerDriver interfaces.\n */\n\nimport type { z } from \"zod\";\nimport type { Finding } from \"./types\";\nimport type { RuntimePayload } from \"./payload-types\";\nimport type { PluginManager } from \"./plugin-manager\";\n\n/**\n * Current driver API version\n */\nexport const DRIVER_API_VERSION = 1;\n\n/**\n * Generic step - drivers define their own step types\n */\nexport interface Step {\n /** Unique step ID */\n id: string;\n\n /** Step type (namespaced, e.g., \"browser.click\", \"api.request\") */\n type: string;\n\n /** Timestamp when step was recorded */\n timestamp: number;\n\n /** Step-specific data */\n [key: string]: unknown;\n}\n\n/**\n * Generic session format\n */\nexport interface Session {\n /** Session name */\n name: string;\n\n /** Driver that recorded this session */\n driver: string;\n\n /** Driver-specific configuration */\n driverConfig: Record<string, unknown>;\n\n /** Recorded steps */\n steps: Step[];\n\n /** Session metadata */\n metadata?: {\n recordedAt?: string;\n version?: string;\n [key: string]: unknown;\n };\n}\n\n/**\n * Recording context passed to drivers\n */\nexport interface RecordContext {\n /** Session being built */\n session: Partial<Session>;\n\n /** Add a step to the session */\n addStep(step: Omit<Step, \"id\" | \"timestamp\">): void;\n\n /** Logger */\n logger: DriverLogger;\n}\n\n/**\n * Running context passed to drivers\n */\nexport interface RunContext {\n /** Session being executed */\n session: Session;\n\n /** Plugin manager for calling hooks */\n pluginManager: PluginManager;\n\n /** Available payloads */\n payloads: RuntimePayload[];\n\n /** Collected findings */\n findings: Finding[];\n\n /** Add a finding */\n addFinding(finding: Finding): void;\n\n /** Logger */\n logger: DriverLogger;\n\n /** Running options */\n options: RunOptions;\n}\n\n/**\n * Options for recording\n */\nexport interface RecordOptions {\n /** Enable auto-crawl mode (driver discovers forms automatically) */\n auto?: boolean;\n\n /** Crawl options (only used when auto=true) */\n crawlOptions?: CrawlOptions;\n\n /** Driver-specific options */\n [key: string]: unknown;\n}\n\n/**\n * Options for auto-crawl mode\n *\n * When a driver supports crawling, these options control how\n * the automated discovery works. Not all drivers support crawling —\n * it's optional and primarily used by the browser driver.\n */\nexport interface CrawlOptions {\n /** Maximum crawl depth (0 = only the given URL, default: 2) */\n maxDepth?: number;\n\n /** Maximum number of pages to visit (default: 20) */\n maxPages?: number;\n\n /** Timeout per page navigation in ms (default: 10000) */\n pageTimeout?: number;\n\n /** Only crawl pages under the same origin (default: true) */\n sameOrigin?: boolean;\n\n /** Playwright storage state JSON for authenticated crawling */\n storageState?: string;\n\n /** Callback when a page is crawled */\n onPageCrawled?: (url: string, formsFound: number) => void;\n}\n\n/**\n * Options for running\n */\nexport interface RunOptions {\n /** Run headless (for visual drivers) */\n headless?: boolean;\n\n /** Callback for findings */\n onFinding?: (finding: Finding) => void;\n\n /** Callback for step completion */\n onStepComplete?: (stepId: string, payloadCount: number) => void;\n\n /**\n * Called by the driver runner after the page/environment is ready.\n * The driver-manager uses this to fire plugin onRunStart hooks\n * with the real page object (instead of null).\n */\n onPageReady?: (page: unknown) => Promise<void>;\n\n /**\n * Called by the driver runner before closing the browser/environment.\n * The driver-manager uses this to fire plugin onBeforeClose hooks\n * so plugins can flush pending async work.\n */\n onBeforeClose?: (page: unknown) => Promise<void>;\n\n /** Driver-specific options */\n [key: string]: unknown;\n}\n\n/**\n * Run result\n */\nexport interface RunResult {\n /** All findings */\n findings: Finding[];\n\n /** Steps executed */\n stepsExecuted: number;\n\n /** Payloads tested */\n payloadsTested: number;\n\n /** Duration in milliseconds */\n duration: number;\n\n /** Errors encountered */\n errors: string[];\n}\n\n/**\n * Driver logger\n */\nexport interface DriverLogger {\n debug(msg: string, ...args: unknown[]): void;\n info(msg: string, ...args: unknown[]): void;\n warn(msg: string, ...args: unknown[]): void;\n error(msg: string, ...args: unknown[]): void;\n}\n\n/**\n * Recorder Driver Interface\n *\n * Implement this to add recording support for a target type.\n */\nexport interface RecorderDriver {\n /** Start recording and return control handle */\n start(\n config: Record<string, unknown>,\n options: RecordOptions,\n ): Promise<RecordingHandle>;\n\n /**\n * Auto-crawl a URL and generate sessions.\n *\n * Optional — only drivers that support automated discovery\n * (e.g., browser) implement this. CLI and API drivers do not.\n *\n * When options.auto=true is passed to startRecording, the engine\n * calls this instead of start().\n */\n crawl?(\n config: Record<string, unknown>,\n options: CrawlOptions,\n ): Promise<Session[]>;\n}\n\n/**\n * Handle returned by RecorderDriver.start()\n */\nexport interface RecordingHandle {\n /** Stop recording and return the session */\n stop(): Promise<Session>;\n\n /** Abort recording without saving */\n abort(): Promise<void>;\n\n /** Get current steps (during recording) */\n getSteps(): Step[];\n\n /** Manually add a step */\n addStep(step: Omit<Step, \"id\" | \"timestamp\">): void;\n}\n\n/**\n * Runner Driver Interface\n *\n * Implement this to add running/replay support for a target type.\n */\nexport interface RunnerDriver {\n /** Execute a session with payloads */\n execute(session: Session, ctx: RunContext): Promise<RunResult>;\n}\n\n/**\n * Complete driver definition\n */\nexport interface VulcnDriver {\n /** Unique driver name (e.g., \"browser\", \"api\", \"cli\") */\n name: string;\n\n /** Driver version */\n version: string;\n\n /** Driver API version */\n apiVersion?: number;\n\n /** Human-readable description */\n description?: string;\n\n /** Configuration schema (Zod) */\n configSchema?: z.ZodSchema;\n\n /** Step types this driver handles */\n stepTypes: string[];\n\n /** Recorder implementation */\n recorder: RecorderDriver;\n\n /** Runner implementation */\n runner: RunnerDriver;\n}\n\n/**\n * Driver source for loading\n */\nexport type DriverSource = \"npm\" | \"local\" | \"builtin\";\n\n/**\n * Loaded driver with metadata\n */\nexport interface LoadedDriver {\n driver: VulcnDriver;\n source: DriverSource;\n}\n","/**\n * Vulcn Plugin Manager\n * Handles plugin loading, lifecycle, and hook execution\n */\n\nimport { readFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { resolve, isAbsolute } from \"node:path\";\nimport YAML from \"yaml\";\nimport { z } from \"zod\";\nimport type {\n VulcnPlugin,\n VulcnConfig,\n PluginConfig,\n LoadedPlugin,\n PluginContext,\n PluginSource,\n PluginLogger,\n EngineInfo,\n PluginHooks,\n} from \"./plugin-types\";\nimport { PLUGIN_API_VERSION } from \"./plugin-types\";\nimport type { Finding } from \"./types\";\nimport type { RuntimePayload } from \"./payload-types\";\n\n// Package version (injected at build time or read from package.json)\nconst ENGINE_VERSION = \"0.2.0\";\n\n/**\n * Config file schema\n */\nconst VulcnConfigSchema = z.object({\n version: z.string().default(\"1\"),\n plugins: z\n .array(\n z.object({\n name: z.string(),\n config: z.record(z.unknown()).optional(),\n enabled: z.boolean().default(true),\n }),\n )\n .optional(),\n settings: z\n .object({\n browser: z.enum([\"chromium\", \"firefox\", \"webkit\"]).optional(),\n headless: z.boolean().optional(),\n timeout: z.number().optional(),\n })\n .optional(),\n});\n\n/**\n * Plugin Manager - loads, configures, and orchestrates plugins\n */\nexport class PluginManager {\n private plugins: LoadedPlugin[] = [];\n private config: VulcnConfig | null = null;\n private initialized = false;\n\n /**\n * Shared context passed to all plugins\n */\n private sharedPayloads: RuntimePayload[] = [];\n private sharedFindings: Finding[] = [];\n\n /**\n * Load configuration from vulcn.config.yml\n */\n async loadConfig(configPath?: string): Promise<VulcnConfig> {\n const paths = configPath\n ? [configPath]\n : [\n \"vulcn.config.yml\",\n \"vulcn.config.yaml\",\n \"vulcn.config.json\",\n \".vulcnrc.yml\",\n \".vulcnrc.yaml\",\n \".vulcnrc.json\",\n ];\n\n for (const path of paths) {\n const resolved = isAbsolute(path) ? path : resolve(process.cwd(), path);\n if (existsSync(resolved)) {\n const content = await readFile(resolved, \"utf-8\");\n const parsed = path.endsWith(\".json\")\n ? JSON.parse(content)\n : YAML.parse(content);\n this.config = VulcnConfigSchema.parse(parsed);\n return this.config;\n }\n }\n\n // No config file - use defaults\n this.config = { version: \"1\", plugins: [], settings: {} };\n return this.config;\n }\n\n /**\n * Load all plugins from config\n */\n async loadPlugins(): Promise<void> {\n if (!this.config) {\n await this.loadConfig();\n }\n\n const pluginConfigs = this.config?.plugins || [];\n\n for (const pluginConfig of pluginConfigs) {\n if (pluginConfig.enabled === false) continue;\n\n try {\n const loaded = await this.loadPlugin(pluginConfig);\n this.plugins.push(loaded);\n } catch (err) {\n console.error(\n `Failed to load plugin ${pluginConfig.name}:`,\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n }\n\n /**\n * Load a single plugin\n */\n private async loadPlugin(config: PluginConfig): Promise<LoadedPlugin> {\n const { name, config: pluginConfig = {} } = config;\n let plugin: VulcnPlugin;\n let source: PluginSource;\n\n // Determine plugin source and load\n if (name.startsWith(\"./\") || name.startsWith(\"../\") || isAbsolute(name)) {\n // Local file plugin\n const resolved = isAbsolute(name) ? name : resolve(process.cwd(), name);\n const module = await import(resolved);\n plugin = module.default || module;\n source = \"local\";\n } else if (name.startsWith(\"@vulcn/\")) {\n // Official plugin (npm package)\n const module = await import(name);\n plugin = module.default || module;\n source = \"npm\";\n } else {\n // Community plugin (npm package)\n const module = await import(name);\n plugin = module.default || module;\n source = \"npm\";\n }\n\n // Validate plugin structure\n this.validatePlugin(plugin);\n\n // Validate plugin config if schema provided\n let resolvedConfig = pluginConfig;\n if (plugin.configSchema) {\n try {\n resolvedConfig = plugin.configSchema.parse(pluginConfig);\n } catch (err) {\n throw new Error(\n `Invalid config for plugin ${name}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n return {\n plugin,\n config: resolvedConfig,\n source,\n enabled: true,\n };\n }\n\n /**\n * Validate plugin structure\n */\n private validatePlugin(plugin: unknown): asserts plugin is VulcnPlugin {\n if (!plugin || typeof plugin !== \"object\") {\n throw new Error(\"Plugin must be an object\");\n }\n\n const p = plugin as Record<string, unknown>;\n if (typeof p.name !== \"string\" || !p.name) {\n throw new Error(\"Plugin must have a name\");\n }\n if (typeof p.version !== \"string\" || !p.version) {\n throw new Error(\"Plugin must have a version\");\n }\n\n // Check API version compatibility\n const apiVersion = (p.apiVersion as number) || 1;\n if (apiVersion > PLUGIN_API_VERSION) {\n throw new Error(\n `Plugin requires API version ${apiVersion}, but engine supports ${PLUGIN_API_VERSION}`,\n );\n }\n }\n\n /**\n * Add a plugin programmatically (for testing or dynamic loading)\n */\n addPlugin(plugin: VulcnPlugin, config: Record<string, unknown> = {}): void {\n this.validatePlugin(plugin);\n this.plugins.push({\n plugin,\n config,\n source: \"custom\",\n enabled: true,\n });\n }\n\n /**\n * Initialize all plugins (call onInit hooks)\n */\n async initialize(): Promise<void> {\n if (this.initialized) return;\n\n // Load payloads from plugins that provide them\n for (const loaded of this.plugins) {\n if (loaded.plugin.payloads) {\n const payloads =\n typeof loaded.plugin.payloads === \"function\"\n ? await loaded.plugin.payloads()\n : loaded.plugin.payloads;\n this.sharedPayloads.push(...payloads);\n }\n }\n\n // Call onInit hooks\n await this.callHook(\"onInit\", (hook, ctx) => hook(ctx));\n\n this.initialized = true;\n }\n\n /**\n * Destroy all plugins (call onDestroy hooks)\n */\n async destroy(): Promise<void> {\n await this.callHook(\"onDestroy\", (hook, ctx) => hook(ctx));\n this.plugins = [];\n this.sharedPayloads = [];\n this.sharedFindings = [];\n this.initialized = false;\n }\n\n /**\n * Get all loaded payloads\n */\n getPayloads(): RuntimePayload[] {\n return this.sharedPayloads;\n }\n\n /**\n * Get all collected findings\n */\n getFindings(): Finding[] {\n return this.sharedFindings;\n }\n\n /**\n * Add a finding (used by detectors)\n */\n addFinding(finding: Finding): void {\n this.sharedFindings.push(finding);\n }\n\n /**\n * Add payloads (used by loaders)\n */\n addPayloads(payloads: RuntimePayload[]): void {\n this.sharedPayloads.push(...payloads);\n }\n\n /**\n * Clear findings (for new run)\n */\n clearFindings(): void {\n this.sharedFindings = [];\n }\n\n /**\n * Get loaded plugins\n */\n getPlugins(): LoadedPlugin[] {\n return this.plugins;\n }\n\n /**\n * Check if a plugin is loaded by name\n */\n hasPlugin(name: string): boolean {\n return this.plugins.some((p) => p.plugin.name === name);\n }\n\n /**\n * Create base context for plugins\n */\n createContext(pluginConfig: Record<string, unknown>): PluginContext {\n const engineInfo: EngineInfo = {\n version: ENGINE_VERSION,\n pluginApiVersion: PLUGIN_API_VERSION,\n };\n\n return {\n config: pluginConfig,\n engine: engineInfo,\n payloads: this.sharedPayloads,\n findings: this.sharedFindings,\n addFinding: (finding: Finding) => {\n this.sharedFindings.push(finding);\n },\n logger: this.createLogger(\"plugin\"),\n fetch: globalThis.fetch,\n };\n }\n\n /**\n * Create scoped logger for a plugin\n */\n private createLogger(name: string): PluginLogger {\n const prefix = `[${name}]`;\n return {\n debug: (msg, ...args) => console.debug(prefix, msg, ...args),\n info: (msg, ...args) => console.info(prefix, msg, ...args),\n warn: (msg, ...args) => console.warn(prefix, msg, ...args),\n error: (msg, ...args) => console.error(prefix, msg, ...args),\n };\n }\n\n /**\n * Call a hook on all plugins sequentially\n */\n async callHook<K extends keyof PluginHooks>(\n hookName: K,\n executor: (\n hook: NonNullable<PluginHooks[K]>,\n ctx: PluginContext,\n ) => Promise<unknown>,\n ): Promise<void> {\n for (const loaded of this.plugins) {\n const hook = loaded.plugin.hooks?.[hookName];\n if (hook) {\n const ctx = this.createContext(loaded.config);\n ctx.logger = this.createLogger(loaded.plugin.name);\n try {\n await executor(hook as NonNullable<PluginHooks[K]>, ctx);\n } catch (err) {\n console.error(\n `Error in plugin ${loaded.plugin.name}.${hookName}:`,\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n }\n }\n\n /**\n * Call a hook and collect results\n */\n async callHookCollect<K extends keyof PluginHooks, R>(\n hookName: K,\n executor: (\n hook: NonNullable<PluginHooks[K]>,\n ctx: PluginContext,\n ) => Promise<R | R[] | null>,\n ): Promise<R[]> {\n const results: R[] = [];\n\n for (const loaded of this.plugins) {\n const hook = loaded.plugin.hooks?.[hookName];\n if (hook) {\n const ctx = this.createContext(loaded.config);\n ctx.logger = this.createLogger(loaded.plugin.name);\n try {\n const result = await executor(\n hook as NonNullable<PluginHooks[K]>,\n ctx,\n );\n if (result !== null && result !== undefined) {\n if (Array.isArray(result)) {\n results.push(...result);\n } else {\n results.push(result);\n }\n }\n } catch (err) {\n console.error(\n `Error in plugin ${loaded.plugin.name}.${hookName}:`,\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n }\n\n return results;\n }\n\n /**\n * Call a hook that transforms a value through the pipeline\n */\n async callHookPipe<T>(\n hookName: keyof PluginHooks,\n initial: T,\n executor: (\n hook: NonNullable<PluginHooks[typeof hookName]>,\n value: T,\n ctx: PluginContext,\n ) => Promise<T>,\n ): Promise<T> {\n let value = initial;\n\n for (const loaded of this.plugins) {\n const hook = loaded.plugin.hooks?.[hookName];\n if (hook) {\n const ctx = this.createContext(loaded.config);\n ctx.logger = this.createLogger(loaded.plugin.name);\n try {\n value = await executor(\n hook as NonNullable<PluginHooks[typeof hookName]>,\n value,\n ctx,\n );\n } catch (err) {\n console.error(\n `Error in plugin ${loaded.plugin.name}.${hookName}:`,\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n }\n\n return value;\n }\n}\n\n/**\n * Default shared plugin manager instance\n */\nexport const pluginManager = new PluginManager();\n","/**\n * Vulcn Plugin System Types\n * @module @vulcn/engine/plugin\n *\n * The plugin system is driver-agnostic. Detection plugins receive\n * a generic page interface rather than Playwright types directly.\n * This allows the same plugin to work across different driver types.\n */\n\nimport type { z } from \"zod\";\nimport type { Session, Step } from \"./driver-types\";\nimport type { Finding } from \"./types\";\nimport type { RunResult } from \"./driver-types\";\nimport type { RuntimePayload, PayloadCategory } from \"./payload-types\";\n\n// Re-export for plugin authors\nexport type {\n Session,\n Step,\n Finding,\n RunResult,\n RuntimePayload,\n PayloadCategory,\n};\n\n/**\n * Plugin API version - plugins declare compatibility\n */\nexport const PLUGIN_API_VERSION = 1;\n\n/**\n * Plugin source types for identification\n */\nexport type PluginSource = \"builtin\" | \"npm\" | \"local\" | \"custom\";\n\n/**\n * Main plugin interface\n */\nexport interface VulcnPlugin {\n /** Unique plugin name (e.g., \"@vulcn/plugin-payloads\") */\n name: string;\n\n /** Plugin version (semver) */\n version: string;\n\n /** Plugin API version this plugin targets */\n apiVersion?: number;\n\n /** Human-readable description */\n description?: string;\n\n /** Lifecycle hooks */\n hooks?: PluginHooks;\n\n /**\n * Payloads provided by this plugin (Loaders)\n * Can be static array or async function for lazy loading\n */\n payloads?: RuntimePayload[] | (() => Promise<RuntimePayload[]>);\n\n /**\n * Zod schema for plugin configuration validation\n */\n configSchema?: z.ZodSchema;\n}\n\n/**\n * Plugin lifecycle hooks\n *\n * Detection hooks (onDialog, onConsoleMessage, etc.) receive\n * Playwright types from the driver. Plugins that use these\n * should declare playwright as a peer/dev dependency.\n */\nexport interface PluginHooks {\n // ─────────────────────────────────────────────────────────────────\n // Initialization\n // ─────────────────────────────────────────────────────────────────\n\n /**\n * Called when plugin is loaded, before any operation\n * Use for setup, loading payloads, etc.\n */\n onInit?: (ctx: PluginContext) => Promise<void>;\n\n /**\n * Called when plugin is unloaded/cleanup\n */\n onDestroy?: (ctx: PluginContext) => Promise<void>;\n\n // ─────────────────────────────────────────────────────────────────\n // Recording Phase\n // ─────────────────────────────────────────────────────────────────\n\n /** Called when recording starts */\n onRecordStart?: (ctx: RecordContext) => Promise<void>;\n\n /** Called for each recorded step, can transform */\n onRecordStep?: (step: Step, ctx: RecordContext) => Promise<Step>;\n\n /** Called when recording ends, can transform session */\n onRecordEnd?: (session: Session, ctx: RecordContext) => Promise<Session>;\n\n // ─────────────────────────────────────────────────────────────────\n // Scan Phase (wraps all sessions)\n // ─────────────────────────────────────────────────────────────────\n\n /** Called once when a scan starts (before any session is executed) */\n onScanStart?: (ctx: ScanContext) => Promise<void>;\n\n /** Called once when a scan ends (after all sessions have executed) */\n onScanEnd?: (result: RunResult, ctx: ScanContext) => Promise<RunResult>;\n\n // ─────────────────────────────────────────────────────────────────\n // Running Phase (per session)\n // ─────────────────────────────────────────────────────────────────\n\n /** Called when run starts */\n onRunStart?: (ctx: RunContext) => Promise<void>;\n\n /** Called before each payload is injected, can transform payload */\n onBeforePayload?: (\n payload: string,\n step: Step,\n ctx: RunContext,\n ) => Promise<string>;\n\n /** Called after payload injection, for detection */\n onAfterPayload?: (ctx: DetectContext) => Promise<Finding[]>;\n\n /**\n * Called before the browser/driver is closed.\n * Plugins should await any pending async work here (e.g., flush\n * in-flight response handlers that need browser access).\n */\n onBeforeClose?: (ctx: PluginContext) => Promise<void>;\n\n /** Called when run ends, can transform results */\n onRunEnd?: (result: RunResult, ctx: RunContext) => Promise<RunResult>;\n\n // ─────────────────────────────────────────────────────────────────\n // Browser Event Hooks (Detection)\n // These receive driver-specific types (e.g. Playwright's Dialog)\n // ─────────────────────────────────────────────────────────────────\n\n /** Called when JavaScript alert/confirm/prompt appears */\n onDialog?: (dialog: unknown, ctx: DetectContext) => Promise<Finding | null>;\n\n /** Called on console.log/warn/error */\n onConsoleMessage?: (\n msg: unknown,\n ctx: DetectContext,\n ) => Promise<Finding | null>;\n\n /** Called on page load/navigation */\n onPageLoad?: (page: unknown, ctx: DetectContext) => Promise<Finding[]>;\n\n /** Called on network request */\n onNetworkRequest?: (\n request: unknown,\n ctx: DetectContext,\n ) => Promise<Finding | null>;\n\n /** Called on network response */\n onNetworkResponse?: (\n response: unknown,\n ctx: DetectContext,\n ) => Promise<Finding | null>;\n}\n\n/**\n * Logger interface for plugins\n */\nexport interface PluginLogger {\n debug: (msg: string, ...args: unknown[]) => void;\n info: (msg: string, ...args: unknown[]) => void;\n warn: (msg: string, ...args: unknown[]) => void;\n error: (msg: string, ...args: unknown[]) => void;\n}\n\n/**\n * Engine information exposed to plugins\n */\nexport interface EngineInfo {\n version: string;\n pluginApiVersion: number;\n}\n\n/**\n * Base context available to all plugin hooks\n */\nexport interface PluginContext {\n /** Plugin-specific config from vulcn.config.yml */\n config: Record<string, unknown>;\n\n /** Engine information */\n engine: EngineInfo;\n\n /** Shared payload registry - loaders add payloads here */\n payloads: RuntimePayload[];\n\n /** Shared findings collection (read-only view, use addFinding to add) */\n findings: Finding[];\n\n /**\n * Add a finding through the proper callback chain.\n * Plugins should use this instead of pushing to findings[] directly,\n * so consumers (CLI, worker) get notified via onFinding callbacks.\n */\n addFinding: (finding: Finding) => void;\n\n /** Scoped logger */\n logger: PluginLogger;\n\n /** Fetch API for network requests */\n fetch: typeof fetch;\n}\n\n/**\n * Context for recording phase hooks\n */\nexport interface RecordContext extends PluginContext {\n /** Page interface (driver-specific, e.g. Playwright Page) */\n page: unknown;\n}\n\n/**\n * Context for running phase hooks\n */\nexport interface RunContext extends PluginContext {\n /** Session being executed */\n session: Session;\n\n /** Page interface (driver-specific, e.g. Playwright Page) */\n page: unknown;\n\n /** Whether running headless */\n headless: boolean;\n}\n\n/**\n * Context for scan-level hooks (wraps all sessions)\n */\nexport interface ScanContext extends PluginContext {\n /** All sessions in this scan */\n sessions: Session[];\n\n /** Whether running headless */\n headless: boolean;\n\n /** Total sessions count */\n sessionCount: number;\n}\n\n/**\n * Context for detection hooks\n */\nexport interface DetectContext extends RunContext {\n /** Current step being tested */\n step: Step;\n\n /** Current payload set being tested */\n payloadSet: RuntimePayload;\n\n /** Actual payload value injected */\n payloadValue: string;\n\n /** Step ID for reporting */\n stepId: string;\n}\n\n/**\n * Plugin configuration in vulcn.config.yml\n */\nexport interface PluginConfig {\n /** Plugin name/path */\n name: string;\n\n /** Plugin-specific configuration */\n config?: Record<string, unknown>;\n\n /** Whether plugin is enabled (default: true) */\n enabled?: boolean;\n}\n\n/**\n * Vulcn configuration file schema\n */\nexport interface VulcnConfig {\n /** Config version */\n version: string;\n\n /** Plugins to load */\n plugins?: PluginConfig[];\n\n /** Global settings */\n settings?: {\n headless?: boolean;\n timeout?: number;\n };\n}\n\n/**\n * Loaded plugin instance with resolved config\n */\nexport interface LoadedPlugin {\n /** Plugin definition */\n plugin: VulcnPlugin;\n\n /** Resolved configuration */\n config: Record<string, unknown>;\n\n /** Source of the plugin */\n source: PluginSource;\n\n /** Whether plugin is enabled */\n enabled: boolean;\n}\n","/**\n * Vulcn Auth Module\n *\n * Handles credential encryption/decryption and auth state management.\n *\n * Security:\n * - AES-256-GCM encryption for credentials at rest\n * - PBKDF2 key derivation from passphrase\n * - Reads passphrase from VULCN_KEY env var (CI/CD) or interactive prompt\n * - Auth state (cookies, localStorage) encrypted separately\n */\n\nimport {\n randomBytes,\n createCipheriv,\n createDecipheriv,\n pbkdf2Sync,\n} from \"node:crypto\";\n\n// ── Types ──────────────────────────────────────────────────────────────\n\n/** Form-based login credentials */\nexport interface FormCredentials {\n type: \"form\";\n username: string;\n password: string;\n /** Custom login URL (if different from target) */\n loginUrl?: string;\n /** Custom CSS selector for username field */\n userSelector?: string;\n /** Custom CSS selector for password field */\n passSelector?: string;\n}\n\n/** Header-based authentication (API keys, Bearer tokens) */\nexport interface HeaderCredentials {\n type: \"header\";\n headers: Record<string, string>;\n}\n\n/** All credential types */\nexport type Credentials = FormCredentials | HeaderCredentials;\n\n/** Auth configuration for a scan */\nexport interface AuthConfig {\n /** Auth strategy */\n strategy: \"storage-state\" | \"header\";\n /** Login page URL */\n loginUrl?: string;\n /** Text that appears when logged in (e.g., \"Logout\") */\n loggedInIndicator?: string;\n /** Text that appears when logged out (e.g., \"Sign In\") */\n loggedOutIndicator?: string;\n /** Session expiry detection rules */\n sessionExpiry?: {\n /** HTTP status codes that indicate session expired */\n statusCodes?: number[];\n /** URL pattern that indicates redirect to login */\n redirectPattern?: string;\n /** Page content that indicates session expired */\n pageContent?: string;\n };\n}\n\n/** Encrypted payload structure (stored as JSON) */\ninterface EncryptedData {\n /** Format version */\n version: 1;\n /** PBKDF2 salt (hex) */\n salt: string;\n /** AES-256-GCM IV (hex) */\n iv: string;\n /** AES-256-GCM auth tag (hex) */\n tag: string;\n /** Encrypted data (hex) */\n data: string;\n /** PBKDF2 iterations */\n iterations: number;\n}\n\n// ── Constants ──────────────────────────────────────────────────────────\n\nconst ALGORITHM = \"aes-256-gcm\";\nconst KEY_LENGTH = 32; // 256 bits\nconst IV_LENGTH = 16; // 128 bits\nconst SALT_LENGTH = 32; // 256 bits\nconst PBKDF2_ITERATIONS = 100_000;\nconst PBKDF2_DIGEST = \"sha512\";\n\n// ── Key Derivation ─────────────────────────────────────────────────────\n\n/**\n * Derive AES-256 key from passphrase using PBKDF2.\n */\nfunction deriveKey(passphrase: string, salt: Buffer): Buffer {\n return pbkdf2Sync(\n passphrase,\n salt,\n PBKDF2_ITERATIONS,\n KEY_LENGTH,\n PBKDF2_DIGEST,\n );\n}\n\n// ── Encryption ─────────────────────────────────────────────────────────\n\n/**\n * Encrypt data with AES-256-GCM.\n *\n * @param data - Plaintext data to encrypt\n * @param passphrase - Passphrase for key derivation\n * @returns JSON string of EncryptedData\n */\nexport function encrypt(data: string, passphrase: string): string {\n const salt = randomBytes(SALT_LENGTH);\n const iv = randomBytes(IV_LENGTH);\n const key = deriveKey(passphrase, salt);\n\n const cipher = createCipheriv(ALGORITHM, key, iv);\n let encrypted = cipher.update(data, \"utf8\", \"hex\");\n encrypted += cipher.final(\"hex\");\n const tag = cipher.getAuthTag();\n\n const payload: EncryptedData = {\n version: 1,\n salt: salt.toString(\"hex\"),\n iv: iv.toString(\"hex\"),\n tag: tag.toString(\"hex\"),\n data: encrypted,\n iterations: PBKDF2_ITERATIONS,\n };\n\n return JSON.stringify(payload);\n}\n\n/**\n * Decrypt data encrypted with encrypt().\n *\n * @param encrypted - JSON string from encrypt()\n * @param passphrase - Passphrase used during encryption\n * @returns Decrypted plaintext\n * @throws Error if passphrase is wrong or data is tampered\n */\nexport function decrypt(encrypted: string, passphrase: string): string {\n const payload: EncryptedData = JSON.parse(encrypted);\n\n if (payload.version !== 1) {\n throw new Error(`Unsupported encryption version: ${payload.version}`);\n }\n\n const salt = Buffer.from(payload.salt, \"hex\");\n const iv = Buffer.from(payload.iv, \"hex\");\n const tag = Buffer.from(payload.tag, \"hex\");\n const key = deriveKey(passphrase, salt);\n\n const decipher = createDecipheriv(ALGORITHM, key, iv);\n decipher.setAuthTag(tag);\n\n let decrypted = decipher.update(payload.data, \"hex\", \"utf8\");\n decrypted += decipher.final(\"utf8\");\n\n return decrypted;\n}\n\n// ── Credential Helpers ─────────────────────────────────────────────────\n\n/**\n * Encrypt credentials to a storable string.\n */\nexport function encryptCredentials(\n credentials: Credentials,\n passphrase: string,\n): string {\n return encrypt(JSON.stringify(credentials), passphrase);\n}\n\n/**\n * Decrypt credentials from a stored string.\n */\nexport function decryptCredentials(\n encrypted: string,\n passphrase: string,\n): Credentials {\n const json = decrypt(encrypted, passphrase);\n return JSON.parse(json) as Credentials;\n}\n\n/**\n * Encrypt browser storage state (cookies, localStorage, etc.).\n * The state is the JSON output from Playwright's context.storageState().\n */\nexport function encryptStorageState(\n storageState: string,\n passphrase: string,\n): string {\n return encrypt(storageState, passphrase);\n}\n\n/**\n * Decrypt browser storage state.\n */\nexport function decryptStorageState(\n encrypted: string,\n passphrase: string,\n): string {\n return decrypt(encrypted, passphrase);\n}\n\n// ── Passphrase Resolution ──────────────────────────────────────────────\n\n/**\n * Get passphrase from environment or throw.\n *\n * In CI/CD, set VULCN_KEY env var.\n * In interactive mode, the CLI should prompt and pass the value here.\n */\nexport function getPassphrase(interactive?: string): string {\n // Interactive passphrase takes priority\n if (interactive) return interactive;\n\n // Fall back to env var\n const envKey = process.env.VULCN_KEY;\n if (envKey) return envKey;\n\n throw new Error(\n \"No passphrase provided. Set VULCN_KEY environment variable or pass --passphrase.\",\n );\n}\n","/**\n * Vulcn Session Format v2\n *\n * Directory-based session format: `.vulcn/` or `<name>.vulcn/`\n *\n * Structure:\n * manifest.yml - scan config, session list, auth config\n * auth/config.yml - login strategy, indicators\n * auth/state.enc - encrypted storageState (cookies/localStorage)\n * sessions/*.yml - individual session files (one per form)\n * requests/*.json - captured HTTP metadata (for Tier 1 fast scan)\n */\n\nimport { readFile, writeFile, mkdir, readdir } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join, basename, extname } from \"node:path\";\nimport { parse, stringify } from \"yaml\";\nimport type { Session } from \"./driver-types\";\nimport type { AuthConfig } from \"./auth\";\n\n// ── Types ──────────────────────────────────────────────────────────────\n\n/** Manifest file schema (manifest.yml) */\nexport interface ScanManifest {\n /** Format version */\n version: \"2\";\n /** Human-readable scan name */\n name: string;\n /** Target URL */\n target: string;\n /** When the scan was recorded */\n recordedAt: string;\n /** Driver name */\n driver: string;\n /** Driver configuration */\n driverConfig: Record<string, unknown>;\n /** Auth configuration (optional) */\n auth?: {\n strategy: string;\n configFile?: string;\n stateFile?: string;\n loggedInIndicator?: string;\n loggedOutIndicator?: string;\n reAuthOn?: Array<Record<string, unknown>>;\n };\n /** Session file references */\n sessions: SessionRef[];\n /** Scan configuration */\n scan?: {\n tier?: \"auto\" | \"http-only\" | \"browser-only\";\n parallel?: number;\n timeout?: number;\n };\n}\n\n/** Reference to a session file within the manifest */\nexport interface SessionRef {\n /** Relative path to session file */\n file: string;\n /** Whether this session has injectable inputs */\n injectable?: boolean;\n}\n\n/** HTTP request metadata for Tier 1 fast scanning */\nexport interface CapturedRequest {\n /** Request method */\n method: string;\n /** Full URL */\n url: string;\n /** Request headers */\n headers: Record<string, string>;\n /** Form data (for POST) */\n body?: string;\n /** Content type */\n contentType?: string;\n /** Which form field is injectable */\n injectableField?: string;\n /** Session name this request belongs to */\n sessionName: string;\n}\n\n// ── Read ──────────────────────────────────────────────────────────────\n\n/**\n * Load a v2 session directory into Session[] ready for execution.\n *\n * @param dirPath - Path to the .vulcn/ directory\n * @returns Array of sessions with manifest metadata attached\n */\nexport async function loadSessionDir(dirPath: string): Promise<{\n manifest: ScanManifest;\n sessions: Session[];\n authConfig?: AuthConfig;\n}> {\n // Read manifest\n const manifestPath = join(dirPath, \"manifest.yml\");\n if (!existsSync(manifestPath)) {\n throw new Error(\n `No manifest.yml found in ${dirPath}. Is this a v2 session directory?`,\n );\n }\n\n const manifestYaml = await readFile(manifestPath, \"utf-8\");\n const manifest = parse(manifestYaml) as ScanManifest;\n\n if (manifest.version !== \"2\") {\n throw new Error(\n `Unsupported session format version: ${manifest.version}. Expected \"2\".`,\n );\n }\n\n // Read auth config if present\n let authConfig: AuthConfig | undefined;\n if (manifest.auth?.configFile) {\n const authPath = join(dirPath, manifest.auth.configFile);\n if (existsSync(authPath)) {\n const authYaml = await readFile(authPath, \"utf-8\");\n authConfig = parse(authYaml) as AuthConfig;\n }\n }\n\n // Read session files\n const sessions: Session[] = [];\n\n for (const ref of manifest.sessions) {\n // Skip non-injectable sessions (e.g., login-only sessions)\n if (ref.injectable === false) continue;\n\n const sessionPath = join(dirPath, ref.file);\n if (!existsSync(sessionPath)) {\n console.warn(`Session file not found: ${sessionPath}, skipping`);\n continue;\n }\n\n const sessionYaml = await readFile(sessionPath, \"utf-8\");\n const sessionData = parse(sessionYaml) as Record<string, unknown>;\n\n // Build full session with manifest-level driver config\n const session: Session = {\n name: (sessionData.name as string) ?? basename(ref.file, \".yml\"),\n driver: manifest.driver,\n driverConfig: {\n ...manifest.driverConfig,\n startUrl: resolveUrl(\n manifest.target,\n sessionData.page as string | undefined,\n ),\n },\n steps: (sessionData.steps as Session[\"steps\"]) ?? [],\n metadata: {\n recordedAt: manifest.recordedAt,\n version: \"2\",\n manifestDir: dirPath,\n },\n };\n\n sessions.push(session);\n }\n\n return { manifest, sessions, authConfig };\n}\n\n/**\n * Check if a path is a v2 session directory.\n */\nexport function isSessionDir(path: string): boolean {\n return existsSync(join(path, \"manifest.yml\"));\n}\n\n/**\n * Check if a path looks like a v2 session directory (by extension).\n */\nexport function looksLikeSessionDir(path: string): boolean {\n return path.endsWith(\".vulcn\") || path.endsWith(\".vulcn/\");\n}\n\n// ── Write ─────────────────────────────────────────────────────────────\n\n/**\n * Save sessions to a v2 session directory.\n *\n * Creates the directory structure:\n * <dirPath>/\n * ├── manifest.yml\n * ├── sessions/\n * │ ├── <session-name>.yml\n * │ └── ...\n * └── requests/ (if HTTP metadata provided)\n * └── ...\n */\nexport async function saveSessionDir(\n dirPath: string,\n options: {\n name: string;\n target: string;\n driver: string;\n driverConfig: Record<string, unknown>;\n sessions: Session[];\n authConfig?: AuthConfig;\n encryptedState?: string;\n requests?: CapturedRequest[];\n },\n): Promise<void> {\n // Create directory structure\n await mkdir(join(dirPath, \"sessions\"), { recursive: true });\n\n const sessionRefs: SessionRef[] = [];\n\n // Write individual session files\n for (const session of options.sessions) {\n const safeName = slugify(session.name);\n const fileName = `sessions/${safeName}.yml`;\n const sessionPath = join(dirPath, fileName);\n\n // Extract the page-relative URL if it starts with the target\n const startUrl = session.driverConfig.startUrl as string | undefined;\n const page = startUrl\n ? startUrl.replace(options.target, \"\").replace(/^\\//, \"/\")\n : undefined;\n\n const sessionData: Record<string, unknown> = {\n name: session.name,\n ...(page ? { page } : {}),\n steps: session.steps,\n };\n\n await writeFile(sessionPath, stringify(sessionData), \"utf-8\");\n\n // Check if session has injectable inputs\n const hasInjectable = session.steps.some(\n (s) =>\n s.type === \"browser.input\" &&\n (s as Record<string, unknown>).injectable !== false,\n );\n\n sessionRefs.push({\n file: fileName,\n injectable: hasInjectable,\n });\n }\n\n // Write auth config if provided\n if (options.authConfig) {\n await mkdir(join(dirPath, \"auth\"), { recursive: true });\n await writeFile(\n join(dirPath, \"auth\", \"config.yml\"),\n stringify(options.authConfig),\n \"utf-8\",\n );\n }\n\n // Write encrypted auth state if provided\n if (options.encryptedState) {\n await mkdir(join(dirPath, \"auth\"), { recursive: true });\n await writeFile(\n join(dirPath, \"auth\", \"state.enc\"),\n options.encryptedState,\n \"utf-8\",\n );\n }\n\n // Write HTTP request metadata if provided\n if (options.requests && options.requests.length > 0) {\n await mkdir(join(dirPath, \"requests\"), { recursive: true });\n for (const req of options.requests) {\n const safeName = slugify(req.sessionName);\n await writeFile(\n join(dirPath, \"requests\", `${safeName}.json`),\n JSON.stringify(req, null, 2),\n \"utf-8\",\n );\n }\n }\n\n // Write manifest\n const manifest: ScanManifest = {\n version: \"2\",\n name: options.name,\n target: options.target,\n recordedAt: new Date().toISOString(),\n driver: options.driver,\n driverConfig: options.driverConfig,\n ...(options.authConfig\n ? {\n auth: {\n strategy: options.authConfig.strategy,\n configFile: \"auth/config.yml\",\n stateFile: options.encryptedState ? \"auth/state.enc\" : undefined,\n loggedInIndicator: options.authConfig.loggedInIndicator,\n loggedOutIndicator: options.authConfig.loggedOutIndicator,\n },\n }\n : {}),\n sessions: sessionRefs,\n scan: {\n tier: \"auto\",\n parallel: 1,\n timeout: 120000,\n },\n };\n\n await writeFile(join(dirPath, \"manifest.yml\"), stringify(manifest), \"utf-8\");\n}\n\n/**\n * Read encrypted auth state from a session directory.\n */\nexport async function readAuthState(dirPath: string): Promise<string | null> {\n const statePath = join(dirPath, \"auth\", \"state.enc\");\n if (!existsSync(statePath)) return null;\n return readFile(statePath, \"utf-8\");\n}\n\n/**\n * Read captured HTTP requests from a session directory.\n */\nexport async function readCapturedRequests(\n dirPath: string,\n): Promise<CapturedRequest[]> {\n const requestsDir = join(dirPath, \"requests\");\n if (!existsSync(requestsDir)) return [];\n\n const files = await readdir(requestsDir);\n const requests: CapturedRequest[] = [];\n\n for (const file of files) {\n if (!file.endsWith(\".json\")) continue;\n const content = await readFile(join(requestsDir, file), \"utf-8\");\n requests.push(JSON.parse(content) as CapturedRequest);\n }\n\n return requests;\n}\n\n// ── Helpers ────────────────────────────────────────────────────────────\n\n/**\n * Resolve a page path against a target URL.\n */\nfunction resolveUrl(target: string, page?: string): string {\n if (!page) return target;\n if (page.startsWith(\"http\")) return page;\n\n // Ensure target doesn't end with / and page starts with /\n const base = target.replace(/\\/$/, \"\");\n const path = page.startsWith(\"/\") ? page : `/${page}`;\n return `${base}${path}`;\n}\n\n/**\n * Convert a string to a safe filename slug.\n */\nfunction slugify(text: string): string {\n return text\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 60);\n}\n"],"mappings":"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","names":["module","pluginManager","import_node_path","import_yaml","YAML","module","import_promises","import_node_fs","import_node_path","import_yaml"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -124,6 +124,10 @@ interface PluginHooks {
|
|
|
124
124
|
onRecordStep?: (step: Step, ctx: RecordContext) => Promise<Step>;
|
|
125
125
|
/** Called when recording ends, can transform session */
|
|
126
126
|
onRecordEnd?: (session: Session, ctx: RecordContext) => Promise<Session>;
|
|
127
|
+
/** Called once when a scan starts (before any session is executed) */
|
|
128
|
+
onScanStart?: (ctx: ScanContext) => Promise<void>;
|
|
129
|
+
/** Called once when a scan ends (after all sessions have executed) */
|
|
130
|
+
onScanEnd?: (result: RunResult, ctx: ScanContext) => Promise<RunResult>;
|
|
127
131
|
/** Called when run starts */
|
|
128
132
|
onRunStart?: (ctx: RunContext$1) => Promise<void>;
|
|
129
133
|
/** Called before each payload is injected, can transform payload */
|
|
@@ -206,6 +210,17 @@ interface RunContext$1 extends PluginContext {
|
|
|
206
210
|
/** Whether running headless */
|
|
207
211
|
headless: boolean;
|
|
208
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Context for scan-level hooks (wraps all sessions)
|
|
215
|
+
*/
|
|
216
|
+
interface ScanContext extends PluginContext {
|
|
217
|
+
/** All sessions in this scan */
|
|
218
|
+
sessions: Session[];
|
|
219
|
+
/** Whether running headless */
|
|
220
|
+
headless: boolean;
|
|
221
|
+
/** Total sessions count */
|
|
222
|
+
sessionCount: number;
|
|
223
|
+
}
|
|
209
224
|
/**
|
|
210
225
|
* Context for detection hooks
|
|
211
226
|
*/
|
|
@@ -450,6 +465,8 @@ interface CrawlOptions {
|
|
|
450
465
|
pageTimeout?: number;
|
|
451
466
|
/** Only crawl pages under the same origin (default: true) */
|
|
452
467
|
sameOrigin?: boolean;
|
|
468
|
+
/** Playwright storage state JSON for authenticated crawling */
|
|
469
|
+
storageState?: string;
|
|
453
470
|
/** Callback when a page is crawled */
|
|
454
471
|
onPageCrawled?: (url: string, formsFound: number) => void;
|
|
455
472
|
}
|
|
@@ -655,6 +672,23 @@ declare class DriverManager {
|
|
|
655
672
|
* via the onPageReady callback, ensuring plugins get a real page object.
|
|
656
673
|
*/
|
|
657
674
|
execute(session: Session, pluginManager: PluginManager, options?: RunOptions): Promise<RunResult>;
|
|
675
|
+
/**
|
|
676
|
+
* Execute multiple sessions with a shared browser (scan-level orchestration).
|
|
677
|
+
*
|
|
678
|
+
* This is the preferred entry point for running a full scan. It:
|
|
679
|
+
* 1. Launches ONE browser for the entire scan
|
|
680
|
+
* 2. Passes the browser to each session's runner via options.browser
|
|
681
|
+
* 3. Each session creates its own context (lightweight, isolated cookies)
|
|
682
|
+
* 4. Aggregates results across all sessions
|
|
683
|
+
* 5. Closes the browser once at the end
|
|
684
|
+
*
|
|
685
|
+
* This is 5-10x faster than calling execute() per session because
|
|
686
|
+
* launching a browser takes 2-3 seconds.
|
|
687
|
+
*/
|
|
688
|
+
executeScan(sessions: Session[], pluginManager: PluginManager, options?: RunOptions): Promise<{
|
|
689
|
+
results: RunResult[];
|
|
690
|
+
aggregate: RunResult;
|
|
691
|
+
}>;
|
|
658
692
|
/**
|
|
659
693
|
* Validate driver structure
|
|
660
694
|
*/
|
|
@@ -669,4 +703,215 @@ declare class DriverManager {
|
|
|
669
703
|
*/
|
|
670
704
|
declare const driverManager: DriverManager;
|
|
671
705
|
|
|
672
|
-
|
|
706
|
+
/**
|
|
707
|
+
* Vulcn Auth Module
|
|
708
|
+
*
|
|
709
|
+
* Handles credential encryption/decryption and auth state management.
|
|
710
|
+
*
|
|
711
|
+
* Security:
|
|
712
|
+
* - AES-256-GCM encryption for credentials at rest
|
|
713
|
+
* - PBKDF2 key derivation from passphrase
|
|
714
|
+
* - Reads passphrase from VULCN_KEY env var (CI/CD) or interactive prompt
|
|
715
|
+
* - Auth state (cookies, localStorage) encrypted separately
|
|
716
|
+
*/
|
|
717
|
+
/** Form-based login credentials */
|
|
718
|
+
interface FormCredentials {
|
|
719
|
+
type: "form";
|
|
720
|
+
username: string;
|
|
721
|
+
password: string;
|
|
722
|
+
/** Custom login URL (if different from target) */
|
|
723
|
+
loginUrl?: string;
|
|
724
|
+
/** Custom CSS selector for username field */
|
|
725
|
+
userSelector?: string;
|
|
726
|
+
/** Custom CSS selector for password field */
|
|
727
|
+
passSelector?: string;
|
|
728
|
+
}
|
|
729
|
+
/** Header-based authentication (API keys, Bearer tokens) */
|
|
730
|
+
interface HeaderCredentials {
|
|
731
|
+
type: "header";
|
|
732
|
+
headers: Record<string, string>;
|
|
733
|
+
}
|
|
734
|
+
/** All credential types */
|
|
735
|
+
type Credentials = FormCredentials | HeaderCredentials;
|
|
736
|
+
/** Auth configuration for a scan */
|
|
737
|
+
interface AuthConfig {
|
|
738
|
+
/** Auth strategy */
|
|
739
|
+
strategy: "storage-state" | "header";
|
|
740
|
+
/** Login page URL */
|
|
741
|
+
loginUrl?: string;
|
|
742
|
+
/** Text that appears when logged in (e.g., "Logout") */
|
|
743
|
+
loggedInIndicator?: string;
|
|
744
|
+
/** Text that appears when logged out (e.g., "Sign In") */
|
|
745
|
+
loggedOutIndicator?: string;
|
|
746
|
+
/** Session expiry detection rules */
|
|
747
|
+
sessionExpiry?: {
|
|
748
|
+
/** HTTP status codes that indicate session expired */
|
|
749
|
+
statusCodes?: number[];
|
|
750
|
+
/** URL pattern that indicates redirect to login */
|
|
751
|
+
redirectPattern?: string;
|
|
752
|
+
/** Page content that indicates session expired */
|
|
753
|
+
pageContent?: string;
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Encrypt data with AES-256-GCM.
|
|
758
|
+
*
|
|
759
|
+
* @param data - Plaintext data to encrypt
|
|
760
|
+
* @param passphrase - Passphrase for key derivation
|
|
761
|
+
* @returns JSON string of EncryptedData
|
|
762
|
+
*/
|
|
763
|
+
declare function encrypt(data: string, passphrase: string): string;
|
|
764
|
+
/**
|
|
765
|
+
* Decrypt data encrypted with encrypt().
|
|
766
|
+
*
|
|
767
|
+
* @param encrypted - JSON string from encrypt()
|
|
768
|
+
* @param passphrase - Passphrase used during encryption
|
|
769
|
+
* @returns Decrypted plaintext
|
|
770
|
+
* @throws Error if passphrase is wrong or data is tampered
|
|
771
|
+
*/
|
|
772
|
+
declare function decrypt(encrypted: string, passphrase: string): string;
|
|
773
|
+
/**
|
|
774
|
+
* Encrypt credentials to a storable string.
|
|
775
|
+
*/
|
|
776
|
+
declare function encryptCredentials(credentials: Credentials, passphrase: string): string;
|
|
777
|
+
/**
|
|
778
|
+
* Decrypt credentials from a stored string.
|
|
779
|
+
*/
|
|
780
|
+
declare function decryptCredentials(encrypted: string, passphrase: string): Credentials;
|
|
781
|
+
/**
|
|
782
|
+
* Encrypt browser storage state (cookies, localStorage, etc.).
|
|
783
|
+
* The state is the JSON output from Playwright's context.storageState().
|
|
784
|
+
*/
|
|
785
|
+
declare function encryptStorageState(storageState: string, passphrase: string): string;
|
|
786
|
+
/**
|
|
787
|
+
* Decrypt browser storage state.
|
|
788
|
+
*/
|
|
789
|
+
declare function decryptStorageState(encrypted: string, passphrase: string): string;
|
|
790
|
+
/**
|
|
791
|
+
* Get passphrase from environment or throw.
|
|
792
|
+
*
|
|
793
|
+
* In CI/CD, set VULCN_KEY env var.
|
|
794
|
+
* In interactive mode, the CLI should prompt and pass the value here.
|
|
795
|
+
*/
|
|
796
|
+
declare function getPassphrase(interactive?: string): string;
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Vulcn Session Format v2
|
|
800
|
+
*
|
|
801
|
+
* Directory-based session format: `.vulcn/` or `<name>.vulcn/`
|
|
802
|
+
*
|
|
803
|
+
* Structure:
|
|
804
|
+
* manifest.yml - scan config, session list, auth config
|
|
805
|
+
* auth/config.yml - login strategy, indicators
|
|
806
|
+
* auth/state.enc - encrypted storageState (cookies/localStorage)
|
|
807
|
+
* sessions/*.yml - individual session files (one per form)
|
|
808
|
+
* requests/*.json - captured HTTP metadata (for Tier 1 fast scan)
|
|
809
|
+
*/
|
|
810
|
+
|
|
811
|
+
/** Manifest file schema (manifest.yml) */
|
|
812
|
+
interface ScanManifest {
|
|
813
|
+
/** Format version */
|
|
814
|
+
version: "2";
|
|
815
|
+
/** Human-readable scan name */
|
|
816
|
+
name: string;
|
|
817
|
+
/** Target URL */
|
|
818
|
+
target: string;
|
|
819
|
+
/** When the scan was recorded */
|
|
820
|
+
recordedAt: string;
|
|
821
|
+
/** Driver name */
|
|
822
|
+
driver: string;
|
|
823
|
+
/** Driver configuration */
|
|
824
|
+
driverConfig: Record<string, unknown>;
|
|
825
|
+
/** Auth configuration (optional) */
|
|
826
|
+
auth?: {
|
|
827
|
+
strategy: string;
|
|
828
|
+
configFile?: string;
|
|
829
|
+
stateFile?: string;
|
|
830
|
+
loggedInIndicator?: string;
|
|
831
|
+
loggedOutIndicator?: string;
|
|
832
|
+
reAuthOn?: Array<Record<string, unknown>>;
|
|
833
|
+
};
|
|
834
|
+
/** Session file references */
|
|
835
|
+
sessions: SessionRef[];
|
|
836
|
+
/** Scan configuration */
|
|
837
|
+
scan?: {
|
|
838
|
+
tier?: "auto" | "http-only" | "browser-only";
|
|
839
|
+
parallel?: number;
|
|
840
|
+
timeout?: number;
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
/** Reference to a session file within the manifest */
|
|
844
|
+
interface SessionRef {
|
|
845
|
+
/** Relative path to session file */
|
|
846
|
+
file: string;
|
|
847
|
+
/** Whether this session has injectable inputs */
|
|
848
|
+
injectable?: boolean;
|
|
849
|
+
}
|
|
850
|
+
/** HTTP request metadata for Tier 1 fast scanning */
|
|
851
|
+
interface CapturedRequest {
|
|
852
|
+
/** Request method */
|
|
853
|
+
method: string;
|
|
854
|
+
/** Full URL */
|
|
855
|
+
url: string;
|
|
856
|
+
/** Request headers */
|
|
857
|
+
headers: Record<string, string>;
|
|
858
|
+
/** Form data (for POST) */
|
|
859
|
+
body?: string;
|
|
860
|
+
/** Content type */
|
|
861
|
+
contentType?: string;
|
|
862
|
+
/** Which form field is injectable */
|
|
863
|
+
injectableField?: string;
|
|
864
|
+
/** Session name this request belongs to */
|
|
865
|
+
sessionName: string;
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Load a v2 session directory into Session[] ready for execution.
|
|
869
|
+
*
|
|
870
|
+
* @param dirPath - Path to the .vulcn/ directory
|
|
871
|
+
* @returns Array of sessions with manifest metadata attached
|
|
872
|
+
*/
|
|
873
|
+
declare function loadSessionDir(dirPath: string): Promise<{
|
|
874
|
+
manifest: ScanManifest;
|
|
875
|
+
sessions: Session[];
|
|
876
|
+
authConfig?: AuthConfig;
|
|
877
|
+
}>;
|
|
878
|
+
/**
|
|
879
|
+
* Check if a path is a v2 session directory.
|
|
880
|
+
*/
|
|
881
|
+
declare function isSessionDir(path: string): boolean;
|
|
882
|
+
/**
|
|
883
|
+
* Check if a path looks like a v2 session directory (by extension).
|
|
884
|
+
*/
|
|
885
|
+
declare function looksLikeSessionDir(path: string): boolean;
|
|
886
|
+
/**
|
|
887
|
+
* Save sessions to a v2 session directory.
|
|
888
|
+
*
|
|
889
|
+
* Creates the directory structure:
|
|
890
|
+
* <dirPath>/
|
|
891
|
+
* ├── manifest.yml
|
|
892
|
+
* ├── sessions/
|
|
893
|
+
* │ ├── <session-name>.yml
|
|
894
|
+
* │ └── ...
|
|
895
|
+
* └── requests/ (if HTTP metadata provided)
|
|
896
|
+
* └── ...
|
|
897
|
+
*/
|
|
898
|
+
declare function saveSessionDir(dirPath: string, options: {
|
|
899
|
+
name: string;
|
|
900
|
+
target: string;
|
|
901
|
+
driver: string;
|
|
902
|
+
driverConfig: Record<string, unknown>;
|
|
903
|
+
sessions: Session[];
|
|
904
|
+
authConfig?: AuthConfig;
|
|
905
|
+
encryptedState?: string;
|
|
906
|
+
requests?: CapturedRequest[];
|
|
907
|
+
}): Promise<void>;
|
|
908
|
+
/**
|
|
909
|
+
* Read encrypted auth state from a session directory.
|
|
910
|
+
*/
|
|
911
|
+
declare function readAuthState(dirPath: string): Promise<string | null>;
|
|
912
|
+
/**
|
|
913
|
+
* Read captured HTTP requests from a session directory.
|
|
914
|
+
*/
|
|
915
|
+
declare function readCapturedRequests(dirPath: string): Promise<CapturedRequest[]>;
|
|
916
|
+
|
|
917
|
+
export { type AuthConfig, type CapturedRequest, type CrawlOptions, type Credentials, type CustomPayload, type CustomPayloadFile, DRIVER_API_VERSION, type DetectContext, type DriverLogger, DriverManager, type DriverSource, type EngineInfo, type Finding, type FormCredentials, type HeaderCredentials, type LoadedDriver, type LoadedPlugin as LoadedPluginInfo, PLUGIN_API_VERSION, type PayloadCategory, type PayloadSource, type PluginConfig, type PluginContext, type PluginHooks, type PluginLogger, PluginManager, type RunContext$1 as PluginRunContext, type PluginSource, type RecordContext, type RecordOptions, type RecorderDriver, type RecordingHandle, type RunContext, type RunOptions, type RunResult, type RunnerDriver, type RuntimePayload, type ScanContext, type ScanManifest, type Session, type SessionRef, type Step, type VulcnConfig, type VulcnDriver, type VulcnPlugin, decrypt, decryptCredentials, decryptStorageState, driverManager, encrypt, encryptCredentials, encryptStorageState, getPassphrase, isSessionDir, loadSessionDir, looksLikeSessionDir, pluginManager, readAuthState, readCapturedRequests, saveSessionDir };
|