diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index b1f5fe0..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8c75cdb --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +[*] +indent_style = space + +[*.{js,ts,jsx,tsx}] +indent_size = 2 +quote_style = double +line_length = 120 diff --git a/.gitignore b/.gitignore index 89b7b15..63a5c74 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules dist -.port \ No newline at end of file +.port +.DS_Store diff --git a/README.md b/README.md index 7448426..19e881d 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ This application is a powerful browser monitoring and interaction tool that enab Read our [docs](https://browsertools.agentdesk.ai/) for the full installation, quickstart and contribution guides. +## Roadmap + +Check out our project roadmap here: [Github Roadmap / Project Board](https://github.com/orgs/AgentDeskAI/projects/1/views/1) + ## Updates v1.2.0 is out! Here's a quick breakdown of the update: @@ -18,18 +22,32 @@ v1.2.0 is out! Here's a quick breakdown of the update: - Improved networking between BrowserTools server, extension and MCP server with host/port auto-discovery, auto-reconnect, and graceful shutdown mechanisms - Added ability to more easily exit out of the Browser Tools server with Ctrl+C - +## Quickstart Guide + +There are three components to run this MCP tool: + +1. Install our chrome extension from here: [v1.2.0 BrowserToolsMCP Chrome Extension](https://github.com/AgentDeskAI/browser-tools-mcp/releases/download/v1.2.0/BrowserTools-1.2.0-extension.zip) +2. Install the MCP server from this command within your IDE: `npx @agentdeskai/browser-tools-mcp@latest` +3. Open a new terminal and run this command: `npx @agentdeskai/browser-tools-server@latest` + +* Different IDEs have different configs but this command is generally a good starting point; please reference your IDEs docs for the proper config setup + +IMPORTANT TIP - there are two servers you need to install. There's... +- browser-tools-server (local nodejs server that's a middleware for gathering logs) +and +- browser-tools-mcp (MCP server that you install into your IDE that communicates w/ the extension + browser-tools-server) -Please make sure to update the version in your IDE / MCP client as so: -`npx @agentdeskai/browser-tools-mcp@1.2.0` +`npx @agentdeskai/browser-tools-mcp@latest` is what you put into your IDE +`npx @agentdeskai/browser-tools-server@latest` is what you run in a new terminal window -Also make sure to download the latest version of the chrome extension here: -[v1.2.0 BrowserToolsMCP Chrome Extension](https://github.com/AgentDeskAI/browser-tools-mcp/releases/download/v1.1.0/chrome-extension-v1-1-0.zip) +After those three steps, open up your chrome dev tools and then the BrowserToolsMCP panel. -From there you can run the local node server as usual like so: -`npx @agentdeskai/browser-tools-server` +If you're still having issues try these steps: +- Quit / close down your browser. Not just the window but all of Chrome itself. +- Restart the local node server (browser-tools-server) +- Make sure you only have ONE instance of chrome dev tools panel open -And once you've opened your chrome dev tools, logs should be getting sent to your server! +After that, it should work but if it doesn't let me know and I can share some more steps to gather logs/info about the issue! If you have any questions or issues, feel free to open an issue ticket! And if you have any ideas to make this better, feel free to reach out or open an issue ticket with an enhancement tag or reach out to me at [@tedx_ai on x](https://x.com/tedx_ai) @@ -56,7 +74,7 @@ Coding agents like Cursor can run these audits against the current page seamless | **SEO** | Evaluates on-page SEO factors (like metadata, headings, and link structure) and suggests improvements for better search visibility. | | **Best Practices** | Checks for general best practices in web development. | | **NextJS Audit** | Injects a prompt used to perform a NextJS audit. | -| **Audit Mode** | Runs all audting tools in a sequence. | +| **Audit Mode** | Runs all auditing tools in a sequence. | | **Debugger Mode** | Runs all debugging tools in a sequence. | --- @@ -153,7 +171,7 @@ Runs all debugging tools in a particular sequence There are three core components all used to capture and analyze browser data: -1. **Chrome Extension**: A browser extension that captures screenshots, console logs, network activity and DOM elements. +1. **Chrome Extension**: A browser extension that captures screenshots, console logs, network activity, DOM elements, and browser storage (cookies, localStorage, sessionStorage). 2. **Node Server**: An intermediary server that facilitates communication between the Chrome extension and any instance of an MCP server. 3. **MCP Server**: A Model Context Protocol server that provides standardized tools for AI clients to interact with the browser. @@ -180,6 +198,7 @@ All consumers of the BrowserTools MCP Server interface with the same NodeJS API - Tracks selected DOM elements - Sends all logs and current element to the BrowserTools Connector - Connects to Websocket server to capture/send screenshots +- Retrieves cookies, localStorage, and sessionStorage data - Allows user to configure token/truncation limits + screenshot folder path #### Node Server @@ -187,6 +206,7 @@ All consumers of the BrowserTools MCP Server interface with the same NodeJS API - Acts as middleware between the Chrome extension and MCP server - Receives logs and currently selected element from Chrome extension - Processes requests from MCP server to capture logs, screenshot or current element +- Retrieves browser storage data (cookies, localStorage, sessionStorage) - Sends Websocket command to the Chrome extension for capturing a screenshot - Intelligently truncates strings and # of duplicate objects in logs to avoid token limits - Removes cookies and sensitive headers to avoid sending to LLMs in MCP clients @@ -211,6 +231,7 @@ Once installed and configured, the system allows any compatible MCP client to: - Capture network traffic - Take screenshots - Analyze selected elements +- Access browser storage (cookies, localStorage, sessionStorage) - Wipe logs stored in our MCP server - Run accessibility, performance, SEO, and best practices audits diff --git a/browser-tools-mcp/README.md b/browser-tools-mcp/README.md index 059ec32..c30474c 100644 --- a/browser-tools-mcp/README.md +++ b/browser-tools-mcp/README.md @@ -9,6 +9,7 @@ A Model Context Protocol (MCP) server that provides AI-powered browser tools int - Network request analysis - Screenshot capture capabilities - Element selection and inspection +- Browser storage access (cookies, localStorage, sessionStorage) - Real-time browser state monitoring - Accessibility, performance, SEO, and best practices audits @@ -52,6 +53,7 @@ npx @agentdeskai/browser-tools-mcp - Element selection - Browser state analysis - Accessibility and performance audits +- Browser cookies, localStorage, and sessionStorage access ## MCP Functions @@ -67,6 +69,9 @@ The server provides the following MCP functions: - `mcp_runPerformanceAudit` - Run a performance audit - `mcp_runSEOAudit` - Run an SEO audit - `mcp_runBestPracticesAudit` - Run a best practices audit +- `mcp_getCookies` - Get cookies from the current page +- `mcp_getLocalStorage` - Get localStorage data +- `mcp_getSessionStorage` - Get sessionStorage data ## Integration diff --git a/browser-tools-mcp/mcp-server.ts b/browser-tools-mcp/mcp-server.ts index a7a1272..6eb0281 100644 --- a/browser-tools-mcp/mcp-server.ts +++ b/browser-tools-mcp/mcp-server.ts @@ -584,7 +584,7 @@ server.tool("runNextJSAudit", {}, async () => ({ text: ` You are an expert in SEO and web development with NextJS. Given the following procedures for analyzing my codebase, please perform a comprehensive - page by page analysis of our NextJS application to identify any issues or areas of improvement for SEO. - After each iteration of changes, reinvoke this tool to re-fetch our SEO audit procedures and then scan our codebase again to identify additional areas of improvement. + After each iteration of changes, reinvoke this tool to re-fetch our SEO audit procedures and then scan our codebase again to identify additional areas of improvement. When no more areas of improvement are found, return "No more areas of improvement found, your NextJS application is optimized for SEO!". @@ -706,7 +706,7 @@ server.tool("runNextJSAudit", {}, async () => ({ initialScale: 1, themeColor: "#ffffff" }; - + export const metadata: Metadata = { metadataBase: new URL("https://dminhvu.com"), openGraph: { @@ -835,25 +835,25 @@ server.tool("runNextJSAudit", {}, async () => ({ type Params = { slug: string; }; - + type Props = { params: Params; searchParams: { [key: string]: string | string[] | undefined }; }; - + export async function generateMetadata( { params, searchParams }: Props, parent: ResolvingMetadata ): Promise { const { slug } = params; - + const post: Post = await fetch("YOUR_ENDPOINT", { method: "GET", next: { revalidate: 60 * 60 * 24 } }).then((res) => res.json()); - + return { title: "{post.title} | dminhvu", authors: [ @@ -903,7 +903,7 @@ server.tool("runNextJSAudit", {}, async () => ({ }; } - + 2. JSON-LD Schema JSON-LD is a format for structured data that can be used by search engines to understand your content. For example, you can use it to describe a person, an event, an organization, a movie, a book, a recipe, and many other types of entities. @@ -912,7 +912,7 @@ server.tool("runNextJSAudit", {}, async () => ({ export default async function Page({ params }) { const { id } = await params const product = await getProduct(id) - + const jsonLd = { '@context': 'https://schema.org', '@type': 'Product', @@ -920,7 +920,7 @@ server.tool("runNextJSAudit", {}, async () => ({ image: product.image, description: product.description, } - + return (
{/* Add JSON-LD to your page */} @@ -932,12 +932,12 @@ server.tool("runNextJSAudit", {}, async () => ({
) } - + You can type your JSON-LD with TypeScript using community packages like schema-dts: import { Product, WithContext } from 'schema-dts' - + const jsonLd: WithContext = { '@context': 'https://schema.org', '@type': 'Product', @@ -981,7 +981,7 @@ server.tool("runNextJSAudit", {}, async () => ({ getAllPostSlugsWithModifyTime } from "@/utils/getData"; import { MetadataRoute } from "next"; - + export default async function sitemap(): Promise { const defaultPages = [ { @@ -1004,10 +1004,10 @@ server.tool("runNextJSAudit", {}, async () => ({ } // other pages ]; - + const postSlugs = await getAllPostSlugsWithModifyTime(); const categorySlugs = await getAllCategories(); - + const sitemap = [ ...defaultPages, ...postSlugs.map((e: any) => ({ @@ -1023,7 +1023,7 @@ server.tool("runNextJSAudit", {}, async () => ({ priority: 0.7 })) ]; - + return sitemap; } With this sitemap.ts file created, you can access the sitemap at https://dminhvu.com/sitemap.xml. @@ -1062,7 +1062,7 @@ server.tool("runNextJSAudit", {}, async () => ({ app/robots.ts import { MetadataRoute } from "next"; - + export default function robots(): MetadataRoute.Robots { return { rules: { @@ -1080,7 +1080,7 @@ server.tool("runNextJSAudit", {}, async () => ({ Allow: / Disallow: /search?q= Disallow: /admin - + Sitemap: https://dminhvu.com/sitemap.xml 5. Link tags Link Tags for Next.js Pages Router @@ -1089,7 +1089,7 @@ server.tool("runNextJSAudit", {}, async () => ({ pages/_app.tsx import Head from "next/head"; - + export default function Page() { return ( @@ -1116,7 +1116,7 @@ server.tool("runNextJSAudit", {}, async () => ({ pages/[slug].tsx import Head from "next/head"; - + export default function Page() { return ( @@ -1191,7 +1191,7 @@ server.tool("runNextJSAudit", {}, async () => ({ import Head from "next/head"; import Script from "next/script"; - + export default function Page() { return ( @@ -1229,7 +1229,7 @@ server.tool("runNextJSAudit", {}, async () => ({ import { GoogleTagManager } from "@next/third-parties/google"; import { GoogleAnalytics } from "@next/third-parties/google"; import Head from "next/head"; - + export default function Page() { return ( @@ -1258,7 +1258,7 @@ server.tool("runNextJSAudit", {}, async () => ({ import Image from "next/image"; - + export default function Page() { return ( { + return await withServerConnection(async () => { + try { + const response = await fetch( + `http://${discoveredHost}:${discoveredPort}/cookies` + ); + + if (!response.ok) { + const errorData = await response.json(); + return { + content: [ + { + type: "text", + text: `Error getting cookies: ${ + errorData.error || response.statusText + }`, + }, + ], + isError: true, + }; + } + + const json = await response.json(); + + if (json.error) { + return { + content: [ + { + type: "text", + text: `Error getting cookies: ${json.error}`, + }, + ], + isError: true, + }; + } + + return { + content: [ + { + type: "text", + text: JSON.stringify(json, null, 2), + }, + ], + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return { + content: [ + { + type: "text", + text: `Error getting cookies: ${message}`, + }, + ], + isError: true, + }; + } + }); +}); + +// Add new tool for getting localStorage +server.tool("getLocalStorage", "Get all localStorage items", async () => { + return await withServerConnection(async () => { + try { + const response = await fetch( + `http://${discoveredHost}:${discoveredPort}/local-storage` + ); + + if (!response.ok) { + const errorData = await response.json(); + return { + content: [ + { + type: "text", + text: `Error getting localStorage: ${ + errorData.error || response.statusText + }`, + }, + ], + isError: true, + }; + } + + const json = await response.json(); + + if (json.error) { + return { + content: [ + { + type: "text", + text: `Error getting localStorage: ${json.error}`, + }, + ], + isError: true, + }; + } + + return { + content: [ + { + type: "text", + text: JSON.stringify(json, null, 2), + }, + ], + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return { + content: [ + { + type: "text", + text: `Error getting localStorage: ${message}`, + }, + ], + isError: true, + }; + } + }); +}); + +// Add new tool for getting sessionStorage +server.tool("getSessionStorage", "Get all sessionStorage items", async () => { + return await withServerConnection(async () => { + try { + const response = await fetch( + `http://${discoveredHost}:${discoveredPort}/session-storage` + ); + + if (!response.ok) { + const errorData = await response.json(); + return { + content: [ + { + type: "text", + text: `Error getting sessionStorage: ${ + errorData.error || response.statusText + }`, + }, + ], + isError: true, + }; + } + + const json = await response.json(); + + if (json.error) { + return { + content: [ + { + type: "text", + text: `Error getting sessionStorage: ${json.error}`, + }, + ], + isError: true, + }; + } + + return { + content: [ + { + type: "text", + text: JSON.stringify(json, null, 2), + }, + ], + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return { + content: [ + { + type: "text", + text: `Error getting sessionStorage: ${message}`, + }, + ], + isError: true, + }; + } + }); +}); + // Start receiving messages on stdio (async () => { try { diff --git a/browser-tools-server/README.md b/browser-tools-server/README.md index 47b59e5..0e16849 100644 --- a/browser-tools-server/README.md +++ b/browser-tools-server/README.md @@ -8,6 +8,7 @@ A powerful browser tools server for capturing and managing browser events, logs, - Network request monitoring - Screenshot capture - Element selection tracking +- Browser storage access (cookies, localStorage, sessionStorage) - WebSocket real-time communication - Configurable log limits and settings - Lighthouse-powered accessibility, performance, SEO, and best practices audits @@ -48,6 +49,9 @@ npx @agentdeskai/browser-tools-server - `/accessibility-audit` - Run accessibility audit on current page - `/performance-audit` - Run performance audit on current page - `/seo-audit` - Run SEO audit on current page +- `/cookies` - Get cookies from the current page +- `/local-storage` - Get localStorage data +- `/session-storage` - Get sessionStorage data ## API Documentation @@ -59,6 +63,9 @@ npx @agentdeskai/browser-tools-server - `GET /network-success` - Returns recent successful network requests - `GET /all-xhr` - Returns all recent network requests - `GET /selected-element` - Returns the currently selected DOM element +- `GET /cookies` - Returns cookies from the current page +- `GET /local-storage` - Returns localStorage data +- `GET /session-storage` - Returns sessionStorage data ### POST Endpoints diff --git a/browser-tools-server/browser-connector.ts b/browser-tools-server/browser-connector.ts index a4cc03c..df050ce 100644 --- a/browser-tools-server/browser-connector.ts +++ b/browser-tools-server/browser-connector.ts @@ -158,6 +158,7 @@ let currentSettings = { queryLimit: 30000, showRequestHeaders: false, showResponseHeaders: false, + sensitiveDataMode: "hide-all", // hide-all, hide-sensitive, show-all model: "claude-3-sonnet", stringSizeLimit: 500, maxLogSize: 20000, @@ -179,7 +180,25 @@ interface ScreenshotCallback { reject: (reason: Error) => void; } +interface CookiesCallback { + resolve: (value: { cookies: any[] }) => void; + reject: (reason: Error) => void; +} + +interface LocalStorageCallback { + resolve: (value: { storage: any }) => void; + reject: (reason: Error) => void; +} + +interface SessionStorageCallback { + resolve: (value: { storage: any }) => void; + reject: (reason: Error) => void; +} + const screenshotCallbacks = new Map(); +const cookiesCallbacks = new Map(); +const localStorageCallbacks = new Map(); +const sessionStorageCallbacks = new Map(); // Function to get available port starting with the given port async function getAvailablePort( @@ -644,6 +663,49 @@ export class BrowserConnector { // Set up Best Practices audit endpoint this.setupBestPracticesAudit(); + // Add endpoint for cookies + this.app.get( + "/cookies", + async (req: express.Request, res: express.Response): Promise => { + console.log("Browser Connector: Received request to /cookies endpoint"); + console.log( + "Browser Connector: Active WebSocket connection:", + !!this.activeConnection + ); + await this.getCookies(req, res); + } + ); + + // Add endpoint for localStorage + this.app.get( + "/local-storage", + async (req: express.Request, res: express.Response): Promise => { + console.log( + "Browser Connector: Received request to /local-storage endpoint" + ); + console.log( + "Browser Connector: Active WebSocket connection:", + !!this.activeConnection + ); + await this.getLocalStorage(req, res); + } + ); + + // Add endpoint for sessionStorage + this.app.get( + "/session-storage", + async (req: express.Request, res: express.Response): Promise => { + console.log( + "Browser Connector: Received request to /session-storage endpoint" + ); + console.log( + "Browser Connector: Active WebSocket connection:", + !!this.activeConnection + ); + await this.getSessionStorage(req, res); + } + ); + // Handle upgrade requests for WebSocket this.server.on( "upgrade", @@ -740,6 +802,90 @@ export class BrowserConnector { new Error(data.error || "Screenshot capture failed") ); screenshotCallbacks.clear(); // Clear all callbacks + } else { + console.log("No callbacks found for screenshot"); + } + } + // Handle cookies data + else if (data.type === "cookies-data" && data.cookies) { + console.log("Received cookies data from extension"); + const callbacks = Array.from(cookiesCallbacks.values()); + if (callbacks.length > 0) { + const callback = callbacks[0]; + callback.resolve({ cookies: data.cookies }); + cookiesCallbacks.clear(); // Clear all callbacks + } + } + // Handle cookies error + else if (data.type === "cookies-error") { + console.log("Received cookies error from extension: ", data.error); + const callbacks = Array.from(cookiesCallbacks.values()); + if (callbacks.length > 0) { + const callback = callbacks[0]; + callback.reject( + new Error(data.error || "Cookies request failed") + ); + cookiesCallbacks.clear(); // Clear all callbacks + } else { + console.log("No callbacks found for cookies"); + } + } + // Handle localStorage data + else if (data.type === "local-storage-data" && data.storage) { + console.log("Received localStorage data from extension"); + const callbacks = Array.from(localStorageCallbacks.values()); + if (callbacks.length > 0) { + const callback = callbacks[0]; + callback.resolve({ storage: data.storage }); + localStorageCallbacks.clear(); // Clear all callbacks + } else { + console.log("No callbacks found for localStorage"); + } + } + // Handle localStorage error + else if (data.type === "local-storage-error") { + console.log( + "Received localStorage error from extension: ", + data.error + ); + const callbacks = Array.from(localStorageCallbacks.values()); + if (callbacks.length > 0) { + const callback = callbacks[0]; + callback.reject( + new Error(data.error || "LocalStorage request failed") + ); + localStorageCallbacks.clear(); // Clear all callbacks + } else { + console.log("No callbacks found for localStorage"); + } + } + // Handle sessionStorage data + else if (data.type === "session-storage-data" && data.storage) { + console.log("Received sessionStorage data from extension"); + const callbacks = Array.from(sessionStorageCallbacks.values()); + if (callbacks.length > 0) { + const callback = callbacks[0]; + callback.resolve({ storage: data.storage }); + sessionStorageCallbacks.clear(); // Clear all callbacks + } else { + console.log("No callbacks found for sessionStorage"); + } + } + // Handle sessionStorage error + else if (data.type === "session-storage-error") { + console.log( + "Received sessionStorage error from extension: ", + data.error + ); + const callbacks = Array.from(sessionStorageCallbacks.values()); + if (callbacks.length > 0) { + const callback = callbacks[0]; + callback.reject( + new Error(data.error || "SessionStorage request failed") + ); + sessionStorageCallbacks.clear(); // Clear all callbacks + } else { + console.log("No callbacks found for sessionStorage"); } } else { console.log("Unhandled message type:", data.type); @@ -1076,7 +1222,7 @@ export class BrowserConnector { const appleScript = ` -- Set path to the screenshot set imagePath to "${fullPath}" - + -- Copy the image to clipboard try set the clipboard to (read (POSIX file imagePath) as «class PNGf») @@ -1084,7 +1230,7 @@ export class BrowserConnector { log "Error copying image to clipboard: " & errMsg return "Failed to copy image to clipboard: " & errMsg end try - + -- Activate Cursor application try tell application "Cursor" @@ -1094,10 +1240,10 @@ export class BrowserConnector { log "Error activating Cursor: " & errMsg return "Failed to activate Cursor: " & errMsg end try - + -- Wait for the application to fully activate delay 3 - + -- Try to interact with Cursor try tell application "System Events" @@ -1106,12 +1252,12 @@ export class BrowserConnector { if (count of windows) is 0 then return "No windows found in Cursor" end if - + set cursorWindow to window 1 - + -- Try Method 1: Look for elements of class "Text Area" set foundElements to {} - + -- Try different selectors to find the text input area try -- Try with class @@ -1120,7 +1266,7 @@ export class BrowserConnector { set foundElements to textAreas end if end try - + if (count of foundElements) is 0 then try -- Try with AXTextField role @@ -1130,7 +1276,7 @@ export class BrowserConnector { end if end try end if - + if (count of foundElements) is 0 then try -- Try with AXTextArea role in nested elements @@ -1149,7 +1295,7 @@ export class BrowserConnector { end repeat end try end if - + -- If no elements found with specific attributes, try a broader approach if (count of foundElements) is 0 then -- Just try to use the Command+V shortcut on the active window @@ -1166,16 +1312,16 @@ export class BrowserConnector { else -- We found a potential text input element set inputElement to item 1 of foundElements - + -- Try to focus and paste try set focused of inputElement to true delay 0.5 - + -- Paste the image keystroke "v" using command down delay 1 - + -- Type the text keystroke "here is the screenshot" delay 1 @@ -1401,6 +1547,228 @@ export class BrowserConnector { } }); } + + // Add method to get cookies + async getCookies(req: express.Request, res: express.Response) { + if (!this.activeConnection) { + console.log( + "Browser Connector: No active WebSocket connection to Chrome extension" + ); + return res.status(503).json({ error: "Chrome extension not connected" }); + } + + try { + console.log("Browser Connector: Getting cookies"); + const requestId = Date.now().toString(); + console.log("Browser Connector: Generated requestId:", requestId); + + // Create promise that will resolve when we get the cookies data + const cookiesPromise = new Promise<{ cookies: any[] }>( + (resolve, reject) => { + console.log( + `Browser Connector: Setting up cookies callback for requestId: ${requestId}` + ); + // Store callback in map + cookiesCallbacks.set(requestId, { resolve, reject }); + console.log( + "Browser Connector: Current callbacks:", + Array.from(cookiesCallbacks.keys()) + ); + + // Set timeout to clean up if we don't get a response + setTimeout(() => { + if (cookiesCallbacks.has(requestId)) { + console.log( + `Browser Connector: Cookies request timed out for requestId: ${requestId}` + ); + cookiesCallbacks.delete(requestId); + reject( + new Error( + "Cookies request timed out - no response from Chrome extension" + ) + ); + } + }, 10000); + } + ); + + // Send cookies request to extension + const message = JSON.stringify({ + type: "get-cookies", + requestId: requestId, + }); + console.log( + `Browser Connector: Sending WebSocket message to extension:`, + message + ); + this.activeConnection.send(message); + + // Wait for cookies data + console.log("Browser Connector: Waiting for cookies data..."); + const result = await cookiesPromise; + console.log( + "Browser Connector: Received cookies data, returning response..." + ); + + // Return the cookies data + res.json(result.cookies); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + console.error("Browser Connector: Error getting cookies:", errorMessage); + return res.status(500).json({ error: errorMessage }); + } + } + + // Add method to get localStorage + async getLocalStorage(req: express.Request, res: express.Response) { + if (!this.activeConnection) { + console.log( + "Browser Connector: No active WebSocket connection to Chrome extension" + ); + return res.status(503).json({ error: "Chrome extension not connected" }); + } + + try { + console.log("Browser Connector: Getting localStorage"); + const requestId = Date.now().toString(); + console.log("Browser Connector: Generated requestId:", requestId); + + // Create promise that will resolve when we get the localStorage data + const localStoragePromise = new Promise<{ storage: any }>( + (resolve, reject) => { + console.log( + `Browser Connector: Setting up localStorage callback for requestId: ${requestId}` + ); + // Store callback in map + localStorageCallbacks.set(requestId, { resolve, reject }); + console.log( + "Browser Connector: Current callbacks:", + Array.from(localStorageCallbacks.keys()) + ); + + // Set timeout to clean up if we don't get a response + setTimeout(() => { + if (localStorageCallbacks.has(requestId)) { + console.log( + `Browser Connector: LocalStorage request timed out for requestId: ${requestId}` + ); + localStorageCallbacks.delete(requestId); + reject( + new Error( + "LocalStorage request timed out - no response from Chrome extension" + ) + ); + } + }, 10000); + } + ); + + // Send localStorage request to extension + const message = JSON.stringify({ + type: "get-local-storage", + requestId: requestId, + }); + console.log( + `Browser Connector: Sending WebSocket message to extension:`, + message + ); + this.activeConnection.send(message); + + // Wait for localStorage data + console.log("Browser Connector: Waiting for localStorage data..."); + const { storage } = await localStoragePromise; + console.log( + "Browser Connector: Received localStorage data, returning response..." + ); + + // Return the localStorage data + res.json(storage); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + console.error( + "Browser Connector: Error getting localStorage:", + errorMessage + ); + return res.status(500).json({ error: errorMessage }); + } + } + + // Add method to get sessionStorage + async getSessionStorage(req: express.Request, res: express.Response) { + if (!this.activeConnection) { + console.log( + "Browser Connector: No active WebSocket connection to Chrome extension" + ); + return res.status(503).json({ error: "Chrome extension not connected" }); + } + + try { + console.log("Browser Connector: Getting sessionStorage"); + const requestId = Date.now().toString(); + console.log("Browser Connector: Generated requestId:", requestId); + + // Create promise that will resolve when we get the sessionStorage data + const sessionStoragePromise = new Promise<{ storage: any }>( + (resolve, reject) => { + console.log( + `Browser Connector: Setting up sessionStorage callback for requestId: ${requestId}` + ); + // Store callback in map + sessionStorageCallbacks.set(requestId, { resolve, reject }); + console.log( + "Browser Connector: Current callbacks:", + Array.from(sessionStorageCallbacks.keys()) + ); + + // Set timeout to clean up if we don't get a response + setTimeout(() => { + if (sessionStorageCallbacks.has(requestId)) { + console.log( + `Browser Connector: SessionStorage request timed out for requestId: ${requestId}` + ); + sessionStorageCallbacks.delete(requestId); + reject( + new Error( + "SessionStorage request timed out - no response from Chrome extension" + ) + ); + } + }, 10000); + } + ); + + // Send sessionStorage request to extension + const message = JSON.stringify({ + type: "get-session-storage", + requestId: requestId, + }); + console.log( + `Browser Connector: Sending WebSocket message to extension:`, + message + ); + this.activeConnection.send(message); + + // Wait for sessionStorage data + console.log("Browser Connector: Waiting for sessionStorage data..."); + const { storage } = await sessionStoragePromise; + console.log( + "Browser Connector: Received sessionStorage data, returning response..." + ); + + // Return the sessionStorage data + res.json(storage); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + console.error( + "Browser Connector: Error getting sessionStorage:", + errorMessage + ); + return res.status(500).json({ error: errorMessage }); + } + } } // Use an async IIFE to allow for async/await in the initial setup diff --git a/chrome-extension/devtools.js b/chrome-extension/devtools.js index 6197f2f..676b654 100644 --- a/chrome-extension/devtools.js +++ b/chrome-extension/devtools.js @@ -8,6 +8,7 @@ let settings = { maxLogSize: 20000, showRequestHeaders: false, showResponseHeaders: false, + sensitiveDataMode: "hide-all", // hide-all, hide-sensitive, show-all screenshotPath: "", // Add new setting for screenshot path serverHost: "localhost", // Default server host serverPort: 3025, // Default server port @@ -21,6 +22,187 @@ const currentTabId = chrome.devtools.inspectedWindow.tabId; const MAX_ATTACH_RETRIES = 3; const ATTACH_RETRY_DELAY = 1000; // 1 second +// Sensitive key patterns - these match keys that typically contain sensitive data +const SENSITIVE_KEY_PATTERNS = [ + // Authentication related + /auth/i, + /token/i, + /jwt/i, + /session/i, + /api[-_]?key/i, + /secret/i, + /password/i, + /pwd/i, + /pass/i, + /credential/i, + /oauth/i, + /refresh[-_]?token/i, + /access[-_]?token/i, + /private[-_]?key/i, + + // Personal information + /ssn/i, + /social[-_]?security/i, + /dob/i, + /birth/i, + /phone/i, + /address/i, + /zip/i, + /postal/i, + /license/i, + /credit[-_]?card/i, + /card[-_]?number/i, + /cvv/i, + /ccv/i, + + // Financial + /bank/i, + /account/i, + /payment/i, + /tax/i, + /salary/i, + /income/i, + + // Health related + /medical/i, + /insurance/i, + /diagnos/i, + + // Other common sensitive data keys + /private/i, + /confidential/i, + /secure/i, + /key/i, +]; + +// Value patterns that might indicate sensitive data regardless of key name +const SENSITIVE_VALUE_PATTERNS = [ + // JWT token pattern (three base64-encoded segments separated by periods) + /^ey[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/, + + // API Keys (various formats) + /^[A-Za-z0-9_-]{16,128}$/, // generic api key + /^AKIA[0-9A-Z]{16}$/, // aws access key + /^[A-Za-z0-9/+=]{40}$/, // aws secret key + /^sk-[A-Za-z0-9]{32,}$/, // popular api keys + /^(sk|pk)_(test|live)_[A-Za-z0-9]{24,}$/, // stripe api key + /^AIza[0-9A-Za-z-_]{35}$/, // google api key + /^gh[pousr]_[A-Za-z0-9_]{36,255}$/, // github token + + // General API key patterns + /^[A-Za-z0-9._-]{32,}$/, + /^[A-Za-z0-9]{8,}[-_][A-Za-z0-9]{4,}[-_][A-Za-z0-9]{4,}[-_][A-Za-z0-9]{4,}[-_][A-Za-z0-9]{12,}$/, + + // OAuth token patterns + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, + /^bearer [A-Za-z0-9._-]+$/i, // oauth bearer token + + // Credit card patterns (simplified, real implementation would use Luhn algorithm) + /^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|6(?:011|5[0-9]{2})[0-9]{12})$/, + + // Social Security Number pattern (US) + /^\d{3}-\d{2}-\d{4}$/, + + // Email addresses + /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, +]; + +// Add entropy calculation helper before isSensitiveValue function +function calculateNormalizedEntropy(str) { + // Create frequency map + const freq = new Map(); + for (const char of str) { + freq.set(char, (freq.get(char) || 0) + 1); + } + + // Calculate entropy using Shannon's formula + let entropy = 0; + const len = str.length; + for (const count of freq.values()) { + const p = count / len; + entropy -= p * Math.log2(p); + } + + // Calculate normalized entropy + const uniqueChars = new Set(str).size; + if (uniqueChars === 0) { + return 0; + } + const maxEntropy = Math.log2(uniqueChars); + return entropy / maxEntropy; +} + +function isSensitiveValue(value) { + // Only check strings + if (typeof value !== "string") return false; + + // Skip very short values + if (value.length < 8) return false; + + // Check against regex patterns first + if (SENSITIVE_VALUE_PATTERNS.some((pattern) => pattern.test(value))) { + return true; + } + + // Entropy-based checks + if (value.length > 16) { + const normalizedEntropy = calculateNormalizedEntropy(value); + // Strings which achieve > 65% of their maximum possible entropy are likely to contain sensitive data + if (normalizedEntropy > 0.65) { + return true; + } + } + + return false; +} + +function isSensitiveKey(key) { + return SENSITIVE_KEY_PATTERNS.some((pattern) => pattern.test(key)); +} + +function filterSensitiveCookies(cookies) { + if (!Array.isArray(cookies)) return []; + + return cookies.map((cookie) => { + if (!cookie || typeof cookie !== "object") return cookie; + + const { name, value } = cookie; + + if ( + settings.sensitiveDataMode === "hide-all" || + (settings.sensitiveDataMode === "hide-sensitive" && + (isSensitiveKey(name) || isSensitiveValue(value))) + ) { + return { + ...cookie, + value: "[SENSITIVE DATA REDACTED]", + }; + } + + return cookie; + }); +} + +function filterSensitiveStorage(storage) { + if (!storage || typeof storage !== "object") return {}; + + const result = {}; + + for (const [key, value] of Object.entries(storage)) { + if ( + settings.sensitiveDataMode === "hide-all" || + (settings.sensitiveDataMode === "hide-sensitive" && + (isSensitiveKey(key) || isSensitiveValue(value))) + ) { + result[key] = "[SENSITIVE DATA REDACTED]"; + } else { + result[key] = value; + } + } + + return result; +} + // Load saved settings on startup chrome.storage.local.get(["browserConnectorSettings"], (result) => { if (result.browserConnectorSettings) { @@ -314,6 +496,7 @@ async function sendToBrowserConnector(logData) { queryLimit: settings.queryLimit, showRequestHeaders: settings.showRequestHeaders, showResponseHeaders: settings.showResponseHeaders, + sensitiveDataMode: settings.sensitiveDataMode, }, }; @@ -1094,6 +1277,175 @@ async function setupWebSocket() { }; requestCurrentUrl(); + } else if (message.type === "get-cookies") { + console.log("Chrome Extension: Getting cookies..."); + // Get cookies from the current tab + chrome.devtools.inspectedWindow.eval( + `(function() { + // Check if document.cookie is empty + if (!document.cookie.trim()) { + return []; + } + + // Split the cookie string and filter out any empty entries + return document.cookie.split(';') + .map(cookie => cookie.trim()) + .filter(cookie => cookie) // Remove empty strings + .map(cookie => { + const equalsPos = cookie.indexOf('='); + // Handle cookies with no value (name only) + if (equalsPos === -1) { + return { name: cookie, value: '' }; + } + // Handle normal cookies with name=value + const name = cookie.substring(0, equalsPos); + const value = cookie.substring(equalsPos + 1); + return { name, value }; + }); + })()`, + (result, isException) => { + if (isException || !result) { + console.error( + "Chrome Extension: Error getting cookies:", + isException + ); + ws.send( + JSON.stringify({ + type: "cookies-error", + error: isException || "Failed to get cookies", + requestId: message.requestId, + }) + ); + return; + } + + console.log( + "Chrome Extension: Cookies retrieved successfully:", + result + ); + + // Make sure cookies is an array, even if empty + let cookies = Array.isArray(result) ? result : []; + + // Filter sensitive data if showSensitive is false + if (settings.sensitiveDataMode !== "show-all") { + console.log( + "Chrome Extension: Filtering sensitive cookie data" + ); + cookies = filterSensitiveCookies(cookies); + } + + ws.send( + JSON.stringify({ + type: "cookies-data", + cookies: cookies, + requestId: message.requestId, + }) + ); + } + ); + } else if (message.type === "get-local-storage") { + console.log("Chrome Extension: Getting localStorage..."); + // Get localStorage from the current tab + chrome.devtools.inspectedWindow.eval( + `(function() { + const storage = {}; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + storage[key] = localStorage.getItem(key); + } + return storage; + })()`, + (result, isException) => { + if (isException || !result) { + console.error( + "Chrome Extension: Error getting localStorage:", + isException + ); + ws.send( + JSON.stringify({ + type: "local-storage-error", + error: isException || "Failed to get localStorage", + requestId: message.requestId, + }) + ); + return; + } + + console.log( + "Chrome Extension: localStorage retrieved successfully:", + result + ); + + // Filter sensitive data if showSensitive is false + let storageData = result; + if (settings.sensitiveDataMode !== "show-all") { + console.log( + "Chrome Extension: Filtering sensitive localStorage data" + ); + storageData = filterSensitiveStorage(result); + } + + ws.send( + JSON.stringify({ + type: "local-storage-data", + storage: storageData, + requestId: message.requestId, + }) + ); + } + ); + } else if (message.type === "get-session-storage") { + console.log("Chrome Extension: Getting sessionStorage..."); + // Get sessionStorage from the current tab + chrome.devtools.inspectedWindow.eval( + `(function() { + const storage = {}; + for (let i = 0; i < sessionStorage.length; i++) { + const key = sessionStorage.key(i); + storage[key] = sessionStorage.getItem(key); + } + return storage; + })()`, + (result, isException) => { + if (isException || !result) { + console.error( + "Chrome Extension: Error getting sessionStorage:", + isException + ); + ws.send( + JSON.stringify({ + type: "session-storage-error", + error: isException || "Failed to get sessionStorage", + requestId: message.requestId, + }) + ); + return; + } + + console.log( + "Chrome Extension: sessionStorage retrieved successfully:", + result + ); + + // Filter sensitive data if showSensitive is false + let storageData = result; + if (settings.sensitiveDataMode !== "show-all") { + console.log( + "Chrome Extension: Filtering sensitive sessionStorage data" + ); + storageData = filterSensitiveStorage(result); + } + + ws.send( + JSON.stringify({ + type: "session-storage-data", + storage: storageData, + requestId: message.requestId, + }) + ); + } + ); } } catch (error) { console.error( diff --git a/chrome-extension/manifest.json b/chrome-extension/manifest.json index 4b94126..c710bb7 100644 --- a/chrome-extension/manifest.json +++ b/chrome-extension/manifest.json @@ -10,7 +10,8 @@ "storage", "tabs", "tabCapture", - "windows" + "windows", + "cookies" ], "host_permissions": [ "" diff --git a/chrome-extension/panel.html b/chrome-extension/panel.html index 5dd04a9..3cb5170 100644 --- a/chrome-extension/panel.html +++ b/chrome-extension/panel.html @@ -113,6 +113,22 @@ .action-button.danger:hover { background-color: #d32f2f; } + .radio-group { + display: flex; + flex-direction: column; + gap: 16px; + } + .radio-option { + margin-bottom: 12px; + } + .radio-option label { + display: flex; + align-items: flex-start; + } + .radio-option input[type="radio"] { + margin-top: 3px; + margin-right: 8px; + } @@ -166,6 +182,32 @@

Server Connection Settings

+
+

Sensitive Data Settings

+
+
+ +
+ +
+ +
+ +
+ +
+
+
+

Advanced Settings

@@ -173,7 +215,7 @@

Advanced Settings

- +
@@ -213,4 +255,4 @@

Advanced Settings

- \ No newline at end of file + diff --git a/chrome-extension/panel.js b/chrome-extension/panel.js index 528df11..4b48355 100644 --- a/chrome-extension/panel.js +++ b/chrome-extension/panel.js @@ -5,6 +5,7 @@ let settings = { stringSizeLimit: 500, showRequestHeaders: false, showResponseHeaders: false, + sensitiveDataMode: "hide-all", // Options: hide-all, hide-sensitive, hide-nothing maxLogSize: 20000, screenshotPath: "", // Add server connection settings @@ -166,12 +167,12 @@ function createConnectionBanner() { const banner = document.createElement("div"); banner.id = "connection-banner"; banner.style.cssText = ` - padding: 6px 0px; + padding: 6px 0px; margin-bottom: 4px; - width: 40%; - display: flex; + width: 40%; + display: flex; flex-direction: column; - align-items: flex-start; + align-items: flex-start; background-color:rgba(0,0,0,0); border-radius: 11px; font-size: 11px; @@ -226,13 +227,13 @@ function createConnectionBanner() { const indicator = document.createElement("div"); indicator.id = "banner-status-indicator"; indicator.style.cssText = ` - width: 6px; - height: 6px; + width: 6px; + height: 6px; position: relative; top: 1px; - border-radius: 50%; - background-color: #ccc; - margin-right: 8px; + border-radius: 50%; + background-color: #ccc; + margin-right: 8px; flex-shrink: 0; transition: background-color 0.3s ease; `; @@ -327,6 +328,11 @@ const connectionStatusDiv = document.getElementById("connection-status"); const statusIcon = document.getElementById("status-icon"); const statusText = document.getElementById("status-text"); +// Sensitive data UI elements +const hideAllRadio = document.getElementById("hide-all-data"); +const hideSensitiveRadio = document.getElementById("hide-sensitive-data"); +const hideNothingRadio = document.getElementById("hide-nothing"); + // Initialize collapsible advanced settings const advancedSettingsHeader = document.getElementById( "advanced-settings-header" @@ -356,6 +362,23 @@ function updateUIFromSettings() { serverHostInput.value = settings.serverHost; serverPortInput.value = settings.serverPort; allowAutoPasteCheckbox.checked = settings.allowAutoPaste; + hideAllRadio.checked = false; + hideSensitiveRadio.checked = false; + hideNothingRadio.checked = false; + switch (settings.sensitiveDataMode) { + case "hide-all": + hideAllRadio.checked = true; + break; + case "hide-sensitive": + hideSensitiveRadio.checked = true; + break; + case "hide-nothing": + hideNothingRadio.checked = true; + break; + default: + // Default to most secure option if setting is invalid + hideAllRadio.checked = true; + } } // Save settings @@ -394,6 +417,27 @@ showResponseHeadersCheckbox.addEventListener("change", (e) => { saveSettings(); }); +hideAllRadio.addEventListener("change", (e) => { + if (e.target.checked) { + settings.sensitiveDataMode = "hide-all"; + saveSettings(); + } +}); + +hideSensitiveRadio.addEventListener("change", (e) => { + if (e.target.checked) { + settings.sensitiveDataMode = "hide-sensitive"; + saveSettings(); + } +}); + +hideNothingRadio.addEventListener("change", (e) => { + if (e.target.checked) { + settings.sensitiveDataMode = "hide-nothing"; + saveSettings(); + } +}); + maxLogSizeInput.addEventListener("change", (e) => { settings.maxLogSize = parseInt(e.target.value, 10); saveSettings();