From f53f3605b4fa31d64158140412859a042e4a3266 Mon Sep 17 00:00:00 2001 From: Sai Smruti Ranjan Das <160756794+saismrutiranjan18@users.noreply.github.com> Date: Tue, 24 Mar 2026 00:51:25 +0530 Subject: [PATCH 1/3] Implement access token and enhance endpoint security Added a one-time access token for secure data fetching and improved endpoint protection. --- .../packages/dashboard/vite.config.ts | 153 +++++++++++++----- 1 file changed, 115 insertions(+), 38 deletions(-) diff --git a/understand-anything-plugin/packages/dashboard/vite.config.ts b/understand-anything-plugin/packages/dashboard/vite.config.ts index aa60f93..2b5ebc5 100644 --- a/understand-anything-plugin/packages/dashboard/vite.config.ts +++ b/understand-anything-plugin/packages/dashboard/vite.config.ts @@ -3,8 +3,21 @@ import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; import path from "path"; import fs from "fs"; +import crypto from "crypto"; + +// Generate a one-time token when the server process starts. +// This token is printed to the terminal and must be in the URL +// to fetch knowledge-graph.json or diff-overlay.json. +const ACCESS_TOKEN = crypto.randomBytes(16).toString("hex"); export default defineConfig({ + // FIX 1 — bind only to localhost, not 0.0.0.0 + // This blocks access from any other device on the same LAN / WiFi. + server: { + host: "127.0.0.1", + port: 5173, + }, + resolve: { alias: { "@understand-anything/core/schema": path.resolve(__dirname, "../core/dist/schema.js"), @@ -12,53 +25,117 @@ export default defineConfig({ "@understand-anything/core/types": path.resolve(__dirname, "../core/dist/types.js"), }, }, + plugins: [ react(), tailwindcss(), { name: "serve-knowledge-graph", configureServer(server) { + // Print the access URL once so the developer can open it. + server.httpServer?.once("listening", () => { + console.log( + `\n 🔑 Dashboard URL: http://127.0.0.1:5173?token=${ACCESS_TOKEN}\n` + ); + }); + server.middlewares.use((req, res, next) => { - if (req.url === "/knowledge-graph.json") { - // GRAPH_DIR env var points to the project being analyzed - // Falls back to monorepo root, then public/ (demo) - const graphDir = process.env.GRAPH_DIR; - const candidates = [ - ...(graphDir - ? [path.resolve(graphDir, ".understand-anything/knowledge-graph.json")] - : []), - path.resolve(process.cwd(), ".understand-anything/knowledge-graph.json"), - path.resolve(process.cwd(), "../../../.understand-anything/knowledge-graph.json"), - ]; - for (const candidate of candidates) { - if (fs.existsSync(candidate)) { - res.setHeader("Content-Type", "application/json"); - fs.createReadStream(candidate).pipe(res); - return; - } - } - } - if (req.url === "/diff-overlay.json") { - const graphDir = process.env.GRAPH_DIR; - const candidates = [ - ...(graphDir - ? [path.resolve(graphDir, ".understand-anything/diff-overlay.json")] - : []), - path.resolve(process.cwd(), ".understand-anything/diff-overlay.json"), - path.resolve(process.cwd(), "../../../.understand-anything/diff-overlay.json"), - ]; - for (const candidate of candidates) { - if (fs.existsSync(candidate)) { - res.setHeader("Content-Type", "application/json"); - fs.createReadStream(candidate).pipe(res); - return; - } - } - res.statusCode = 404; - res.end(); + const url = new URL(req.url ?? "/", "http://127.0.0.1:5173"); + const pathname = url.pathname; + const isProtectedEndpoint = + pathname === "/knowledge-graph.json" || + pathname === "/diff-overlay.json"; + + if (!isProtectedEndpoint) { + next(); return; } - next(); + + // FIX 3 — require the one-time token on all data endpoints. + // Requests without a matching ?token= get a 403. + if (url.searchParams.get("token") !== ACCESS_TOKEN) { + res.statusCode = 403; + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({ error: "Forbidden: missing or invalid token" })); + return; + } + + const fileName = + pathname === "/diff-overlay.json" + ? "diff-overlay.json" + : "knowledge-graph.json"; + + const graphDir = process.env.GRAPH_DIR; + const candidates = [ + ...(graphDir + ? [path.resolve(graphDir, `.understand-anything/${fileName}`)] + : []), + path.resolve(process.cwd(), `.understand-anything/${fileName}`), + path.resolve( + process.cwd(), + `../../../.understand-anything/${fileName}` + ), + ]; + + for (const candidate of candidates) { + if (!fs.existsSync(candidate)) continue; + + // FIX 2 — sanitise absolute file paths before sending the JSON. + // Nodes can contain filePath values like /Users/alice/company/src/auth.ts. + // We convert those to relative paths (src/auth.ts) so the developer's + // home directory and company directory layout are not leaked. + try { + const raw = JSON.parse(fs.readFileSync(candidate, "utf-8")) as { + nodes?: Array>; + [key: string]: unknown; + }; + + // Derive the project root from the candidate path so we can + // make file paths relative to it. + const projectRoot = path.dirname( + candidate.replace( + `${path.sep}.understand-anything${path.sep}${fileName}`, + "" + ) + ); + + if (Array.isArray(raw.nodes)) { + raw.nodes = raw.nodes.map((node) => { + if (typeof node.filePath !== "string") return node; + const abs = node.filePath; + // Only relativise paths that actually sit inside projectRoot. + // Leave external or already-relative paths untouched. + const rel = abs.startsWith(projectRoot) + ? abs.slice(projectRoot.length).replace(/^[\\/]/, "") + : path.isAbsolute(abs) + ? path.basename(abs) // absolute but outside root — use filename only + : abs; // already relative — keep as-is + return { ...node, filePath: rel }; + }); + } + + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify(raw)); + } catch (err) { + // If we cannot parse or sanitise the file, refuse to serve it + // rather than accidentally leaking raw content. + console.error("[understand-anything] Failed to sanitise graph file:", err); + res.statusCode = 500; + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({ error: "Failed to read graph file" })); + } + return; + } + + // No matching file found on disk. + if (pathname === "/diff-overlay.json") { + res.statusCode = 404; + res.end(); + } else { + res.statusCode = 404; + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({ error: "No knowledge graph found. Run /understand first." })); + } }); }, }, From 99bc056c26cb19647a1c88a1dea1b91b3bf3dd16 Mon Sep 17 00:00:00 2001 From: Sai Smruti Ranjan Das <160756794+saismrutiranjan18@users.noreply.github.com> Date: Tue, 24 Mar 2026 00:52:49 +0530 Subject: [PATCH 2/3] Specify host in Vite dev server command Updated Vite dev server command to specify host. --- understand-anything-plugin/skills/understand-dashboard/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/understand-anything-plugin/skills/understand-dashboard/SKILL.md b/understand-anything-plugin/skills/understand-dashboard/SKILL.md index e0614d7..4c20dbb 100644 --- a/understand-anything-plugin/skills/understand-dashboard/SKILL.md +++ b/understand-anything-plugin/skills/understand-dashboard/SKILL.md @@ -56,7 +56,7 @@ Start the Understand Anything dashboard to visualize the knowledge graph for the 5. Start the Vite dev server pointing at the project's knowledge graph: ```bash - cd && GRAPH_DIR= npx vite --open + cd && GRAPH_DIR= npx vite --host 127.0.0.1 --open ``` Run this in the background so the user can continue working. From f9e5f551b583d8304b523303c581401967e17ba7 Mon Sep 17 00:00:00 2001 From: Sai Smruti Ranjan Das <160756794+saismrutiranjan18@users.noreply.github.com> Date: Tue, 24 Mar 2026 00:55:53 +0530 Subject: [PATCH 3/3] Update index.ts --- .../packages/core/src/persistence/index.ts | 69 ++++++++++++++++++- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/understand-anything-plugin/packages/core/src/persistence/index.ts b/understand-anything-plugin/packages/core/src/persistence/index.ts index 36ba5cf..5eb7105 100644 --- a/understand-anything-plugin/packages/core/src/persistence/index.ts +++ b/understand-anything-plugin/packages/core/src/persistence/index.ts @@ -1,5 +1,5 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs"; -import { join } from "node:path"; +import { join, isAbsolute, relative, basename } from "node:path"; import type { KnowledgeGraph, AnalysisMeta } from "../types.js"; import { validateGraph } from "../schema.js"; @@ -15,9 +15,68 @@ function ensureDir(projectRoot: string): string { return dir; } +/** + * Sanitise every node's filePath before writing to disk. + * + * The analysis agent produces absolute paths like: + * /Users/alice/company/src/auth.ts + * + * We convert them to paths relative to projectRoot: + * src/auth.ts + * + * Three cases are handled: + * 1. Path is inside projectRoot → make it relative + * 2. Path is absolute but outside → keep only the filename (last segment) + * 3. Path is already relative → leave it untouched + * + * This means the developer's home directory, username, and company + * directory layout are never written to knowledge-graph.json. + */ +function sanitiseFilePaths( + graph: KnowledgeGraph, + projectRoot: string, +): KnowledgeGraph { + const normalRoot = projectRoot.endsWith("/") + ? projectRoot + : projectRoot + "/"; + + const sanitisedNodes = graph.nodes.map((node) => { + if (typeof node.filePath !== "string") return node; + + const fp = node.filePath; + + if (!isAbsolute(fp)) { + // Already relative — nothing to do. + return node; + } + + if (fp.startsWith(normalRoot) || fp.startsWith(projectRoot)) { + // Inside the project root — make it relative. + return { ...node, filePath: relative(projectRoot, fp) }; + } + + // Absolute but outside the project root — use only the filename + // so we leak as little as possible. + return { ...node, filePath: basename(fp) }; + }); + + return { ...graph, nodes: sanitisedNodes }; +} + export function saveGraph(projectRoot: string, graph: KnowledgeGraph): void { const dir = ensureDir(projectRoot); - writeFileSync(join(dir, GRAPH_FILE), JSON.stringify(graph, null, 2), "utf-8"); + + // FIX — sanitise absolute file paths before persisting. + // Without this, absolute paths like /Users/alice/company/src/auth.ts + // are written verbatim into knowledge-graph.json and later served + // by the dashboard server, leaking the developer's directory layout. + const sanitised = sanitiseFilePaths(graph, projectRoot); + + writeFileSync( + join(dir, GRAPH_FILE), + JSON.stringify(sanitised, null, 2), + "utf-8", + ); } export function loadGraph( @@ -44,7 +103,11 @@ export function loadGraph( export function saveMeta(projectRoot: string, meta: AnalysisMeta): void { const dir = ensureDir(projectRoot); - writeFileSync(join(dir, META_FILE), JSON.stringify(meta, null, 2), "utf-8"); + writeFileSync( + join(dir, META_FILE), + JSON.stringify(meta, null, 2), + "utf-8", + ); } export function loadMeta(projectRoot: string): AnalysisMeta | null {