From f3d14989322c20f1129e60f15706d43295964e2f Mon Sep 17 00:00:00 2001 From: Satvik Bhatnagar Date: Sun, 25 Jan 2026 22:35:11 -0800 Subject: [PATCH 01/11] Get Enclosing Entity for Token Optimization --- commands/security/analyze-new.toml | 118 +++ mcp-server/package-lock.json | 764 +++++++++++++++++- mcp-server/package.json | 4 +- mcp-server/src/codemaps/graph_builder.ts | 89 ++ mcp-server/src/codemaps/graph_service.ts | 123 +++ mcp-server/src/codemaps/index.ts | 9 + mcp-server/src/codemaps/models.ts | 21 + .../src/codemaps/parsers/base_parser.ts | 13 + mcp-server/src/codemaps/parsers/go_parser.ts | 230 ++++++ .../src/codemaps/parsers/javascript_parser.ts | 296 +++++++ .../src/codemaps/parsers/python_parser.ts | 153 ++++ .../src/codemaps/parsers/typescript_parser.ts | 383 +++++++++ mcp-server/src/index.ts | 70 +- 13 files changed, 2268 insertions(+), 5 deletions(-) create mode 100644 commands/security/analyze-new.toml create mode 100644 mcp-server/src/codemaps/graph_builder.ts create mode 100644 mcp-server/src/codemaps/graph_service.ts create mode 100644 mcp-server/src/codemaps/index.ts create mode 100644 mcp-server/src/codemaps/models.ts create mode 100644 mcp-server/src/codemaps/parsers/base_parser.ts create mode 100644 mcp-server/src/codemaps/parsers/go_parser.ts create mode 100644 mcp-server/src/codemaps/parsers/javascript_parser.ts create mode 100644 mcp-server/src/codemaps/parsers/python_parser.ts create mode 100644 mcp-server/src/codemaps/parsers/typescript_parser.ts diff --git a/commands/security/analyze-new.toml b/commands/security/analyze-new.toml new file mode 100644 index 0000000..5cd3db4 --- /dev/null +++ b/commands/security/analyze-new.toml @@ -0,0 +1,118 @@ +description = "Analyzes code changes on your current branch for common security vulnerabilities using a diff-based approach" +prompt = """You are a highly skilled senior security analyst. Your primary task is to conduct a security audit of the current pull request. +Utilizing your skillset, you must operate by strictly following the operating principles defined in your context. + + +## Skillset: Taint Analysis & The Diff-Based Investigation Model + +This is your primary technique for identifying injection-style vulnerabilities (`SQLi`, `XSS`, `Command Injection`, etc.) and other data-flow-related issues. You **MUST** apply this technique within the **Diff-Based Analysis Workflow**. + +The core principle is to trace untrusted data from its entry point (**Source**) to a location where it is executed or rendered (**Sink**). A vulnerability exists if the data is not properly sanitized or validated on its path from the Source to the Sink. + +## Core Operational Loop: The Diff-Based Analysis Workflow + +This workflow focuses on analyzing the diff of a pull request to identify potential security risks efficiently. + +#### Step 1: Retrieve PR Diff +Instead of just getting a list of changed files, the workflow will retrieve the full diff for the pull request. The diff shows the specific lines that were added, modified, or removed. + +#### Step 2: Analyze Diff for Taint Sources +The agent will analyze only the diff content, which is significantly smaller than the full content of all changed files. +It will focus specifically on newly added or modified lines (lines prefixed with +) to identify patterns that introduce a potential "source" of untrusted input (e.g., handling of req.query, req.body, file uploads, or other external data). + +#### Step 3: Build a Targeted Investigation Plan +A file will be added to the SECURITY_ANALYSIS_TODO.md for a full, deep-dive investigation only if its diff contains a potential taint source. +Files with benign changes (e.g., comment updates, dependency bumps in lockfiles, documentation changes) will be ignored entirely. Their content will never be read or processed by the agent. + +#### Step 4: Perform Deep-Dive Investigation +The agent proceeds with its deep-dive analysis as before, but only on the much smaller, pre-qualified list of files that have been identified as containing legitimate security risks. +This diff-first approach ensures that the most expensive part of the process—reading and analyzing entire files—is reserved for the few files that actually require it. + +For EVERY task, you MUST follow this procedure. + +1. **Phase 0: Initial Planning** + * **Action:** First, understand the high-level task from the user's prompt. + * **Action:** If it does not already exist, create a new folder named `.gemini_security_new` in the user's workspace. + * **Action:** Create a new file named `SECURITY_ANALYSIS_TODO.md` in `.gemini_security_new`, and write the initial, high-level objectives from the prompt into it. + * **Action:** Create a new, empty file named `DRAFT_SECURITY_REPORT.md` in `.gemini_security_new`. + +2. **Phase 1: Dynamic Execution & Planning** + * **Action:** Read the `SECURITY_ANALYSIS_TODO.md` file and execute the first task about determining the scope of the analysis (getting the diff). + * **Action (Plan Refinement):** After identifying the scope, analyze the diff for taint sources. Rewrite `SECURITY_ANALYSIS_TODO.md` to replace the generic "analyze files" task with specific **Investigation Tasks** for each file that contains a potential taint source (e.g., `- [ ] Investigate data flow from [variable] in fileA.js`). + +3. **Phase 2: The Investigation Loop** + * This is the core execution loop for analyzing the identified files. + * Execute each investigation task, performing the deep-dive analysis (e.g., tracing the variable, checking for sanitization). + * If an investigation confirms a vulnerability, **append the finding to `DRAFT_SECURITY_REPORT.md`**. + * Mark the investigation task as done (`[x]`). + * **Action:** Repeat this loop until all investigation tasks are complete. + +4. **Phase 3: Final Review & Refinement** + * **Action:** This phase begins when all analysis tasks in `SECURITY_ANALYSIS_TODO.md` are complete. + * **Action:** Read the entire `DRAFT_SECURITY_REPORT.md` file. + * **Action:** Critically review **every single finding** in the draft against the **"High-Fidelity Reporting & Minimizing False Positives"** principles and its five-question checklist. + * **Action:** You must use the `gemini-cli-security` MCP server to get the line numbers for each finding. For each vulnerability you have found, you must call the `find_line_numbers` tool with the `filePath` and the `snippet` of the vulnerability. You will then add the `startLine` and `endLine` to the final report. + * **Action:** Construct the final, clean report in your memory. + +5. **Phase 4: Final Reporting & Cleanup** + * **Action:** Output the final, reviewed report as your response to the user. + * **Action:** If, after the review, no vulnerabilities remain, your final output **MUST** be the standard "clean report" message specified by the task prompt. + * **Action:** Remove the temporary files (`SECURITY_ANALYSIS_TODO.md` and `DRAFT_SECURITY_REPORT.md`) from the `.gemini_security/` directory unless instructed otherwise. Only remove these files and do not remove any other user files under any circumstances. + +### Example of the Workflow in `SECURITY_ANALYSIS_TODO.md` + +1. **Initial State:** + ```markdown + - [ ] Define the audit scope. + ``` +2. **After Scope Definition (Diff Analysis):** The agent gets the diff and finds `+ const userId = req.query.id;` in `userController.js`. It rewrites `SECURITY_ANALYSIS_TODO.md`: + ```markdown + - [x] Define the audit scope. + - [ ] Analyze diff for taint sources and create investigation plan. + - [ ] Investigate data flow from `userId` in `userController.js`. + ``` +3. **Investigation Pass Begins:** The model now executes the sub-task. It traces `userId` and finds it is used in `db.run("SELECT * FROM users WHERE id = " + userId);`. It confirms this is an SQL Injection vulnerability, adds the finding to `DRAFT_SECURITY_REPORT.md`, and marks the task as complete. + +## Analysis Instructions + +**Step 1: Initial Planning** + +Your first action is to create a `SECURITY_ANALYSIS_TODO.md` file with the following exact, high-level plan. This initial plan is fixed and must not be altered. When writing files always use absolute paths (e.g., `/path/to/file`). + +- [ ] Define the audit scope. +- [ ] Analyze diff for taint sources and create investigation plan. +- [ ] Conduct deep-dive SAST analysis on identified files. +- [ ] Conduct the final review of all findings as per your **Minimizing False Positives** operating principle and generate the final report. + +**Step 2: Execution Directives** + +You will now begin executing the plan. The following are your precise instructions to start with. + +1. **To complete the 'Define the audit scope' task:** + * You **MUST** run the exact command: `git rev-parse --is-inside-work-tree`. + * If the above command succeeds, returning true: then proceed to step 1a. + * If the above command fails, producing a fatal error: then proceed to step 1b. + +1a. **To define the audit scope in a git repository** + * You **MUST** run the exact command: `git diff -U10 --merge-base origin/HEAD | grep -v '^-'`. + * If this command fails and does not produce a changelist, use this exact command: `git diff -U10 | grep -v '^-'`. + * This is your only method for determining the diff. Do not use any other commands for this purpose. + * Once the command is executed and you have the diff, you will mark this task as complete. + +1b. **To define the audit scope in a non-git folder** + * Let the user know that you were unable to generate an automatic changelist with git, so you **MUST** prompt the user for files to security scan. + * Match the users response to files in the workspace and build a list of files to analyze. + * This is your only method for determining the files to analyze. Do not use any other commands for this purpose. + * Once you have a list of files to analyze you will mark this task as complete. + +2. **Immediately after defining the scope, you must refine your plan:** + * You will rewrite the `SECURITY_ANALYSIS_TODO.md` file. + * You will analyze the diff content, focusing on added/modified lines (prefixed with `+`). + * For each file where the diff introduces a potential taint source, you **MUST** add a specific **"Investigate data flow from [variable] in [file]"** task. + * Files with benign changes (e.g., comments, documentation) should be ignored. + * Out of Scope Files: Files that are primarily used for managing dependencies like lockfiles (e.g., `package-lock.json`, `package.json` `yarn.lock`, `go.sum`) should be considered out of scope and **must be omitted from the plan entirely**, as they contain no actionable code to review. + * You **MUST** replace the line `- [ ] Analyze diff for taint sources and create investigation plan.` with the specific investigation tasks you identified. + +After completing these initial tasks, continue executing the dynamically generated plan according to your **Core Operational Loop**. + +Proceed with the Initial Planning Phase now.""" \ No newline at end of file diff --git a/mcp-server/package-lock.json b/mcp-server/package-lock.json index edacc10..ab9b958 100644 --- a/mcp-server/package-lock.json +++ b/mcp-server/package-lock.json @@ -7,6 +7,8 @@ "name": "gemini-cli-security-mcp-server", "dependencies": { "@modelcontextprotocol/sdk": "^1.24.0", + "tree-sitter": "^0.21.0", + "web-tree-sitter": "^0.22.0", "zod": "^3.24.2" }, "devDependencies": { @@ -15,10 +17,401 @@ "vitest": "^3.2.4" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", + "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/android-arm/-/android-arm-0.25.11.tgz", + "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", + "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/android-x64/-/android-x64-0.25.11.tgz", + "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/darwin-arm64": { "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", - "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", + "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", + "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", + "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", + "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", + "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", + "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", + "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", + "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", + "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", + "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", + "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", + "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", + "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", + "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", + "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", + "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", + "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", + "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", + "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", + "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", "cpu": [ "arm64" ], @@ -26,7 +419,41 @@ "license": "MIT", "optional": true, "os": [ - "darwin" + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", + "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.11", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", + "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" ], "engines": { "node": ">=18" @@ -76,6 +503,34 @@ } } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", + "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", + "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.52.5", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", @@ -90,6 +545,272 @@ "darwin" ] }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", + "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", + "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", + "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", + "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", + "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", + "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", + "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", + "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", + "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", + "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", + "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", + "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", + "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", + "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", + "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", + "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", + "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", + "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.5", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", + "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -1074,6 +1795,26 @@ "node": ">= 0.6" } }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1639,6 +2380,17 @@ "node": ">=0.6" } }, + "node_modules/tree-sitter": { + "version": "0.21.1", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/tree-sitter/-/tree-sitter-0.21.1.tgz", + "integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.0.0", + "node-gyp-build": "^4.8.0" + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -1863,6 +2615,12 @@ } } }, + "node_modules/web-tree-sitter": { + "version": "0.22.6", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/web-tree-sitter/-/web-tree-sitter-0.22.6.tgz", + "integrity": "sha512-hS87TH71Zd6mGAmYCvlgxeGDjqd9GTeqXNqTT+u0Gs51uIozNIaaq/kUAbV/Zf56jb2ZOyG8BxZs2GG9wbLi6Q==", + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/mcp-server/package.json b/mcp-server/package.json index 3369633..1a6c07a 100644 --- a/mcp-server/package.json +++ b/mcp-server/package.json @@ -17,6 +17,8 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.24.0", - "zod": "^3.24.2" + "zod": "^3.24.2", + "tree-sitter": "^0.21.0", + "web-tree-sitter": "^0.22.0" } } \ No newline at end of file diff --git a/mcp-server/src/codemaps/graph_builder.ts b/mcp-server/src/codemaps/graph_builder.ts new file mode 100644 index 0000000..7535138 --- /dev/null +++ b/mcp-server/src/codemaps/graph_builder.ts @@ -0,0 +1,89 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import Parser from 'web-tree-sitter'; +import { GraphService } from './graph_service.js'; +import { GraphNode } from './models.js'; +import { PythonParser } from './parsers/python_parser.js'; +import { JavaScriptParser } from './parsers/javascript_parser.js'; +import { GoParser } from './parsers/go_parser.js'; +import { TypeScriptParser } from './parsers/typescript_parser.js'; +import { LanguageParser } from './parsers/base_parser.js'; +import { promises as fs } from 'fs'; + +export class GraphBuilder { + private parser!: Parser; + private languageParsers: { [key: string]: LanguageParser }; + private languageLibs: { [key: string]: string }; + + constructor(private graphService: GraphService) { + this.languageParsers = { + python: new PythonParser(graphService), + javascript: new JavaScriptParser(graphService), + go: new GoParser(graphService), + typescript: new TypeScriptParser(graphService), + }; + this.languageLibs = { + python: 'tree-sitter-python.wasm', + javascript: 'tree-sitter-javascript.wasm', + go: 'tree-sitter-go.wasm', + typescript: 'tree-sitter-typescript.wasm', + }; + } + + public async buildGraph(filePath: string) { + const language = this._getLanguageFromFileExtension(filePath); + const languageLibPath = this.languageLibs[language]; + if (!languageLibPath) { + throw new Error(`Unsupported language: ${language}`); + } + + await Parser.init(); + const Lang = await Parser.Language.load(languageLibPath); + this.parser = new Parser(); + this.parser.setLanguage(Lang); + + const fileContent = await fs.readFile(filePath, 'utf8'); + const tree = this.parser.parse(fileContent); + + const fileNode: GraphNode = { + id: filePath, + type: 'file', + name: filePath, + startLine: 0, + endLine: 0, + documentation: '', + codeSnippet: '', + }; + this.graphService.addNode(fileNode); + + const languageParser = this.languageParsers[language]; + this._traverseTree(tree.rootNode, languageParser, filePath, filePath); + + return this.graphService.graph; + } + + private _traverseTree(node: Parser.SyntaxNode, languageParser: LanguageParser, filePath: string, scope: string) { + const newScope = languageParser.parse(node, filePath, scope); + for (const child of node.children) { + this._traverseTree(child, languageParser, filePath, newScope); + } + } + + private _getLanguageFromFileExtension(filePath: string): string { + if (filePath.endsWith('.py')) { + return 'python'; + } else if (filePath.endsWith('.js')) { + return 'javascript'; + } else if (filePath.endsWith('.ts') || filePath.endsWith('.tsx')) { + return 'typescript'; + } else if (filePath.endsWith('.go')) { + return 'go'; + } else { + throw new Error(`Unsupported file extension: ${filePath}`); + } + } +} diff --git a/mcp-server/src/codemaps/graph_service.ts b/mcp-server/src/codemaps/graph_service.ts new file mode 100644 index 0000000..937ffc5 --- /dev/null +++ b/mcp-server/src/codemaps/graph_service.ts @@ -0,0 +1,123 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { GraphNode, GraphEdge } from './models.js'; + +export class GraphService { + public graph: { + nodes: Map; + edges: Map; // Adjacency list for outgoing edges + inEdges: Map; // For incoming edges + }; + private _byName: Map>; + private _byFileAndName: Map; + public _pendingCalls: [string, string, string][]; + + constructor() { + this.graph = { + nodes: new Map(), + edges: new Map(), + inEdges: new Map(), + }; + this._byName = new Map(); + this._byFileAndName = new Map(); + this._pendingCalls = []; + } + + private _indexNode(nodeId: string, nodeData: GraphNode) { + const name = nodeData.name; + if (name) { + if (!this._byName.has(name)) { + this._byName.set(name, new Set()); + } + this._byName.get(name)!.add(nodeId); + + const filePath = nodeId.split(':', 1)[0]; + this._byFileAndName.set(`${filePath}:${name}`, nodeId); + } + } + + public addNode(node: GraphNode) { + this.graph.nodes.set(node.id, node); + this._indexNode(node.id, node); + } + + public addEdge(edge: GraphEdge) { + if (!this.graph.edges.has(edge.source)) { + this.graph.edges.set(edge.source, []); + } + this.graph.edges.get(edge.source)!.push(edge); + + if (!this.graph.inEdges.has(edge.target)) { + this.graph.inEdges.set(edge.target, []); + } + this.graph.inEdges.get(edge.target)!.push(edge); + } + + public findEnclosingEntity(filePath: string, lineNumber: number): GraphNode | null { + const enclosingNodes: GraphNode[] = []; + for (const node of this.graph.nodes.values()) { + if (node.id.startsWith(filePath) && node.type !== 'file') { + if (node.startLine <= lineNumber && node.endLine >= lineNumber) { + enclosingNodes.push(node); + } + } + } + + if (enclosingNodes.length === 0) { + return null; + } + + // Find the most specific entity (smallest line range) + return enclosingNodes.reduce((mostSpecific, current) => { + const specificRange = mostSpecific.endLine - mostSpecific.startLine; + const currentRange = current.endLine - current.startLine; + return currentRange < specificRange ? current : mostSpecific; + }); + } + + public querySymbol(name: string, filePath?: string): GraphNode | null { + if (filePath) { + const key = `${filePath}:${name}`; + if (this._byFileAndName.has(key)) { + const nodeId = this._byFileAndName.get(key); + if (nodeId) { + return this.graph.nodes.get(nodeId) || null; + } + } + } + const ids = this._byName.get(name); + if (ids && ids.size === 1) { + const nodeId = ids.values().next().value; + if (nodeId) { + return this.graph.nodes.get(nodeId) || null; + } + } + // Ambiguous or not found + return null; + } + + public ensureModuleNode(moduleName: string): string { + const nodeId = `module:${moduleName}`; + if (!this.graph.nodes.has(nodeId)) { + const node: GraphNode = { + id: nodeId, + type: 'module', + name: moduleName, + startLine: 0, + endLine: 0, + documentation: '', + codeSnippet: '', + }; + this.addNode(node); + } + return nodeId; + } + + public addPendingCall(filePath: string, sourceId: string, calleeName: string) { + this._pendingCalls.push([filePath, sourceId, calleeName]); + } +} diff --git a/mcp-server/src/codemaps/index.ts b/mcp-server/src/codemaps/index.ts new file mode 100644 index 0000000..fa00e46 --- /dev/null +++ b/mcp-server/src/codemaps/index.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './models.js'; +export * from './graph_service.js'; +export * from './graph_builder.js'; diff --git a/mcp-server/src/codemaps/models.ts b/mcp-server/src/codemaps/models.ts new file mode 100644 index 0000000..2afff19 --- /dev/null +++ b/mcp-server/src/codemaps/models.ts @@ -0,0 +1,21 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export interface GraphNode { + id: string; + type: string; + name: string; + startLine: number; + endLine: number; + documentation: string; + codeSnippet: string; +} + +export interface GraphEdge { + source: string; + target: string; + type: string; +} diff --git a/mcp-server/src/codemaps/parsers/base_parser.ts b/mcp-server/src/codemaps/parsers/base_parser.ts new file mode 100644 index 0000000..0fa2639 --- /dev/null +++ b/mcp-server/src/codemaps/parsers/base_parser.ts @@ -0,0 +1,13 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SyntaxNode } from 'web-tree-sitter'; +import { GraphService } from '../graph_service.js'; + +export interface LanguageParser { + graphService: GraphService; + parse(node: SyntaxNode, filePath: string, scope: string): string; +} diff --git a/mcp-server/src/codemaps/parsers/go_parser.ts b/mcp-server/src/codemaps/parsers/go_parser.ts new file mode 100644 index 0000000..2064e2a --- /dev/null +++ b/mcp-server/src/codemaps/parsers/go_parser.ts @@ -0,0 +1,230 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SyntaxNode } from 'web-tree-sitter'; +import { GraphService } from '../graph_service.js'; +import { GraphNode, GraphEdge } from '../models.js'; +import { LanguageParser } from './base_parser.js'; + +export class GoParser implements LanguageParser { + constructor(public graphService: GraphService) {} + + parse(node: SyntaxNode, filePath: string, scope: string): string { + let newScope = scope; + + if (node.type === 'function_declaration') { + const nameNode = node.childForFieldName('name'); + if (nameNode) { + const name = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const documentation = this._getDocstring(node); + const codeSnippet = node.text; + const nodeId = `${filePath}:${name}`; + + this.graphService.addNode({ + id: nodeId, + type: 'function', + name, + startLine, + endLine, + documentation, + codeSnippet, + }); + newScope = nodeId; + if (scope) { + this.graphService.addEdge({ source: scope, target: nodeId, type: 'contains' }); + } + } + } else if (node.type === 'type_declaration') { + for (const spec of this._findAll(node, 'type_spec')) { + this._handleTypeSpec(spec, filePath); + } + } else if (node.type === 'call_expression') { + const callee = this._extractCalleeName(node); + if (callee && scope) { + const calleeNode = this.graphService.querySymbol(callee); + if (calleeNode) { + this.graphService.addEdge({ source: scope, target: calleeNode.id, type: 'calls' }); + } else { + this.graphService.addPendingCall(filePath, scope, callee); + } + } + } else if (node.type === 'import_declaration') { + for (const spec of this._findAll(node, 'import_spec')) { + const pathNode = spec.childForFieldName('path'); + let moduleName: string | null = null; + if (pathNode) { + moduleName = this._stripQuotes(pathNode.text); + } else { + const lit = this._firstStringLiteral(spec); + if (lit) { + moduleName = this._stripQuotes(lit); + } + } + + if (moduleName) { + const targetId = this.graphService.ensureModuleNode(moduleName); + this.graphService.addEdge({ source: filePath, target: targetId, type: 'imports' }); + } + } + } + + return newScope; + } + + private _handleTypeSpec(specNode: SyntaxNode, filePath: string) { + const nameNode = specNode.childForFieldName('name'); + const typeNode = specNode.childForFieldName('type'); + if (!nameNode || !typeNode) { + return; + } + + if (typeNode.type !== 'struct_type') { + return; + } + + const name = nameNode.text; + const startLine = specNode.startPosition.row + 1; + const endLine = specNode.endPosition.row + 1; + const codeSnippet = specNode.text; + const nodeId = `${filePath}:${name}`; + + this.graphService.addNode({ + id: nodeId, + type: 'struct', + name, + startLine, + endLine, + documentation: '', + codeSnippet, + }); + this.graphService.addEdge({ source: filePath, target: nodeId, type: 'contains' }); + + for (const fieldDecl of this._findAll(typeNode, 'field_declaration')) { + if (this._hasChildType(fieldDecl, 'field_identifier')) { + continue; + } + + const typeChild = fieldDecl.childForFieldName('type'); + let parentName: string | null = null; + if (!typeChild) { + parentName = this._rightmostIdentifier(fieldDecl); + } else { + parentName = this._rightmostIdentifier(typeChild); + } + + if (!parentName) { + continue; + } + + const parentNode = this.graphService.querySymbol(parentName); + if (parentNode) { + this.graphService.addEdge({ source: nodeId, target: parentNode.id, type: 'inherits' }); + } + } + } + + private _extractCalleeName(callNode: SyntaxNode): string | null { + if (callNode.type !== 'call_expression') { + return null; + } + const fn = callNode.childForFieldName('function'); + if (!fn) { + return null; + } + + if (fn.type === 'identifier') { + return fn.text; + } + + if (fn.type === 'selector_expression') { + const field = fn.childForFieldName('field'); + if (field) { + return field.text; + } + return this._rightmostIdentifier(fn); + } + + return null; + } + + private _rightmostIdentifier(node: SyntaxNode): string | null { + if ( + node.type === 'identifier' || + node.type === 'type_identifier' || + node.type === 'field_identifier' + ) { + return node.text; + } + + if (node.type === 'selector_expression') { + const fld = node.childForFieldName('field'); + if (fld) { + return fld.text; + } + } + + if (node.namedChildCount > 0) { + for (let i = node.namedChildCount - 1; i >= 0; i--) { + const name = this._rightmostIdentifier(node.namedChildren[i]); + if (name) { + return name; + } + } + } + return null; + } + + private _findAll(node: SyntaxNode, typeName: string): SyntaxNode[] { + const out: SyntaxNode[] = []; + const stack: SyntaxNode[] = [node]; + while (stack.length > 0) { + const cur = stack.pop()!; + for (const ch of cur.children) { + if (ch.type === typeName) { + out.push(ch); + } + stack.push(ch); + } + } + return out; + } + + private _hasChildType(node: SyntaxNode, typeName: string): boolean { + return node.children.some((ch) => ch.type === typeName); + } + + private _firstStringLiteral(node: SyntaxNode): string | null { + const stack: SyntaxNode[] = [node]; + while (stack.length > 0) { + const cur = stack.pop()!; + for (const ch of cur.children) { + if ( + ch.type === 'interpreted_string_literal' || + ch.type === 'raw_string_literal' || + ch.type === 'string_literal' + ) { + return ch.text; + } + stack.push(ch); + } + } + return null; + } + + private _stripQuotes(s: string): string { + return s.length >= 2 && (s.startsWith("'") || s.startsWith('"')) ? s.slice(1, -1) : s; + } + + private _getDocstring(node: SyntaxNode): string { + const prev = node.previousSibling; + if (prev && prev.type === 'comment') { + return prev.text; + } + return ''; + } +} diff --git a/mcp-server/src/codemaps/parsers/javascript_parser.ts b/mcp-server/src/codemaps/parsers/javascript_parser.ts new file mode 100644 index 0000000..9cf9c09 --- /dev/null +++ b/mcp-server/src/codemaps/parsers/javascript_parser.ts @@ -0,0 +1,296 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SyntaxNode } from 'web-tree-sitter'; +import { GraphService } from '../graph_service.js'; +import { GraphNode, GraphEdge } from '../models.js'; +import { LanguageParser } from './base_parser.js'; + +export class JavaScriptParser implements LanguageParser { + constructor(public graphService: GraphService) {} + + parse(node: SyntaxNode, filePath: string, scope: string): string { + let newScope = scope; + + if (node.type === 'function_declaration') { + const nameNode = node.childForFieldName('name'); + if (nameNode) { + const name = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const documentation = this._getDocstring(node); + const codeSnippet = node.text; + const nodeId = `${filePath}:${name}`; + + this.graphService.addNode({ + id: nodeId, + type: 'function', + name, + startLine, + endLine, + documentation, + codeSnippet, + }); + newScope = nodeId; + + if (scope) { + this.graphService.addEdge({ source: scope, target: nodeId, type: 'contains' }); + } + } + } else if (node.type === 'class_declaration') { + const nameNode = node.childForFieldName('name'); + if (nameNode) { + const name = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const documentation = this._getDocstring(node); + const codeSnippet = node.text; + const nodeId = `${filePath}:${name}`; + + this.graphService.addNode({ + id: nodeId, + type: 'class', + name, + startLine, + endLine, + documentation, + codeSnippet, + }); + newScope = nodeId; + if (scope) { + this.graphService.addEdge({ source: scope, target: nodeId, type: 'contains' }); + } + + const parentName = this._getParentJavaScriptClassDeclaration(node); + if (parentName) { + const parentNode = this.graphService.querySymbol(parentName); + if (parentNode) { + this.graphService.addEdge({ source: nodeId, target: parentNode.id, type: 'inherits' }); + } + } + } + } else if (node.type === 'method_definition') { + const nameNode = node.children.find( + (child) => child.type === 'property_identifier' || child.type === 'private_property_identifier' + ); + if (nameNode) { + const methodName = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const documentation = this._getDocstring(node); + const codeSnippet = node.text; + const nodeId = `${filePath}:${methodName}`; + + this.graphService.addNode({ + id: nodeId, + type: 'function', + name: methodName, + startLine, + endLine, + documentation, + codeSnippet, + }); + + if (scope) { + this.graphService.addEdge({ source: scope, target: nodeId, type: 'contains' }); + } + + newScope = nodeId; + } + } else if (node.type === 'call_expression') { + if (this._maybeAddCommonJsImport(node, filePath)) { + return newScope; + } + + const calleeName = this._getCalleeName(node); + if (calleeName && scope) { + const calleeNode = this.graphService.querySymbol(calleeName); + if (calleeNode) { + this.graphService.addEdge({ source: scope, target: calleeNode.id, type: 'calls' }); + } else { + this.graphService.addPendingCall(filePath, scope, calleeName); + } + } + } else if (node.type === 'variable_declarator') { + const nameNode = node.childForFieldName('name'); + const valueNode = node.childForFieldName('value'); + + if (nameNode && nameNode.type === 'identifier') { + const varName = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const codeSnippet = node.text; + const varId = `${filePath}:${varName}`; + this.graphService.addNode({ + id: varId, + type: 'variable', + name: varName, + startLine, + endLine, + documentation: '', + codeSnippet, + }); + if (scope) { + this.graphService.addEdge({ source: scope, target: varId, type: 'contains' }); + } + } + + if ( + nameNode && + nameNode.type === 'identifier' && + valueNode && + (valueNode.type === 'function_expression' || valueNode.type === 'arrow_function') + ) { + const funcName = nameNode.text; + const startLine = valueNode.startPosition.row + 1; + const endLine = valueNode.endPosition.row + 1; + const documentation = this._getDocstring(node); + const codeSnippet = valueNode.text; + const funcId = `${filePath}:${funcName}`; + this.graphService.addNode({ + id: funcId, + type: 'function', + name: funcName, + startLine, + endLine, + documentation, + codeSnippet, + }); + if (scope) { + this.graphService.addEdge({ source: scope, target: funcId, type: 'contains' }); + } + } + } else if (node.type === 'import_statement') { + const sourceNode = node.childForFieldName('source'); + if (sourceNode) { + let moduleName = sourceNode.text; + if (moduleName.length >= 2 && (moduleName.startsWith("'") || moduleName.startsWith('"'))) { + moduleName = moduleName.slice(1, -1); + } + const targetId = this.graphService.ensureModuleNode(moduleName); + this.graphService.addEdge({ source: filePath, target: targetId, type: 'imports' }); + } + } + + return newScope; + } + + private _getDocstring(node: SyntaxNode): string { + let prev = node.previousSibling; + while (prev && prev.type === 'comment') { + if (prev.text.startsWith('/**')) { + return prev.text; + } + prev = prev.previousSibling; + } + return ''; + } + + private _getParentJavaScriptClassDeclaration(node: SyntaxNode): string | null { + const classHeritageNode = node.children.find((child) => child.type === 'class_heritage'); + if (classHeritageNode && classHeritageNode.namedChildCount > 0) { + const base = classHeritageNode.namedChildren[0]; + if (base.type === 'identifier') { + return base.text; + } else if (base.type === 'member_expression') { + const prop = base.childForFieldName('property'); + if (prop) { + return prop.text; + } + } + } + return null; + } + + private _getCalleeName(callNode: SyntaxNode): string | null { + const fn = callNode.childForFieldName('function'); + if (!fn) { + return null; + } + + if (fn.type === 'identifier') { + return fn.text; + } + + if (fn.type === 'member_expression') { + const prop = fn.childForFieldName('property'); + if (prop && prop.type === 'property_identifier') { + return prop.text; + } + if (prop && prop.type === 'identifier') { + return prop.text; + } + return null; + } + + if (fn.type === 'optional_chain') { + let inner = fn; + while (inner && inner.namedChildCount > 0) { + const last = inner.namedChildren[inner.namedChildCount - 1]; + if (last.type === 'member_expression' || last.type === 'identifier') { + inner = last; + break; + } + inner = last; + } + if (inner.type === 'identifier') { + return inner.text; + } + if (inner.type === 'member_expression') { + const prop = inner.childForFieldName('property'); + if (prop && (prop.type === 'property_identifier' || prop.type === 'identifier')) { + return prop.text; + } + } + return null; + } + + return null; + } + + private _maybeAddCommonJsImport(callNode: SyntaxNode, filePath: string): boolean { + if (callNode.type !== 'call_expression') { + return false; + } + + const fn = callNode.childForFieldName('function'); + if (!fn || fn.type !== 'identifier' || fn.text !== 'require') { + return false; + } + + const args = callNode.childForFieldName('arguments'); + if (!args || args.namedChildCount === 0) { + return false; + } + + const first = args.namedChildren[0]; + + if (first.type === 'string') { + let moduleName = first.text; + if (moduleName.length >= 2 && (moduleName.startsWith("'") || moduleName.startsWith('"'))) { + moduleName = moduleName.slice(1, -1); + } + const targetId = this.graphService.ensureModuleNode(moduleName); + this.graphService.addEdge({ source: filePath, target: targetId, type: 'imports' }); + return true; + } + + if (first.type === 'template_string') { + const raw = first.text; + if (!raw.includes('${')) { + let moduleName = raw; + if (moduleName.length >= 2 && moduleName.startsWith('`') && moduleName.endsWith('`')) { + moduleName = moduleName.slice(1, -1); + } + const targetId = this.graphService.ensureModuleNode(moduleName); + this.graphService.addEdge({ source: filePath, target: targetId, type: 'imports' }); + return true; + } + } + + return false; + } +} diff --git a/mcp-server/src/codemaps/parsers/python_parser.ts b/mcp-server/src/codemaps/parsers/python_parser.ts new file mode 100644 index 0000000..7b1c016 --- /dev/null +++ b/mcp-server/src/codemaps/parsers/python_parser.ts @@ -0,0 +1,153 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SyntaxNode } from 'web-tree-sitter'; +import { GraphService } from '../graph_service.js'; +import { GraphNode, GraphEdge } from '../models.js'; +import { LanguageParser } from './base_parser.js'; + +export class PythonParser implements LanguageParser { + constructor(public graphService: GraphService) {} + + parse(node: SyntaxNode, filePath: string, scope: string): string { + let newScope = scope; + if (node.type === 'function_definition') { + const nameNode = node.childForFieldName('name'); + if (nameNode) { + const name = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const documentation = this._getDocstring(node); + const codeSnippet = node.text; + const nodeId = `${filePath}:${name}`; + + const newNode: GraphNode = { + id: nodeId, + type: 'function', + name, + startLine, + endLine, + documentation, + codeSnippet, + }; + this.graphService.addNode(newNode); + newScope = nodeId; + if (scope) { + this.graphService.addEdge({ source: scope, target: nodeId, type: 'contains' }); + } + } + } else if (node.type === 'class_definition') { + const nameNode = node.childForFieldName('name'); + if (nameNode) { + const name = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const documentation = this._getDocstring(node); + const codeSnippet = node.text; + const nodeId = `${filePath}:${name}`; + + const newNode: GraphNode = { + id: nodeId, + type: 'class', + name, + startLine, + endLine, + documentation, + codeSnippet, + }; + this.graphService.addNode(newNode); + newScope = nodeId; + if (scope) { + this.graphService.addEdge({ source: scope, target: nodeId, type: 'contains' }); + } + + const superclassesNode = node.childForFieldName('superclasses'); + if (superclassesNode) { + for (const superclass of superclassesNode.children) { + if (superclass.type === 'identifier') { + const parentName = superclass.text; + const parentNode = this.graphService.querySymbol(parentName); + if (parentNode) { + this.graphService.addEdge({ source: nodeId, target: parentNode.id, type: 'inherits' }); + } + } + } + } + } + } else if (node.type === 'call') { + const calleeName = this._pyCalleeName(node); + if (calleeName && scope) { + const calleeNode = this.graphService.querySymbol(calleeName); + if (calleeNode) { + this.graphService.addEdge({ source: scope, target: calleeNode.id, type: 'calls' }); + } else { + this.graphService.addPendingCall(filePath, scope, calleeName); + } + } + } else if (node.type === 'import_statement') { + for (const alias of node.namedChildren) { + if (alias.type === 'dotted_name') { + const moduleName = alias.text; + const targetId = this.graphService.ensureModuleNode(moduleName); + this.graphService.addEdge({ source: filePath, target: targetId, type: 'imports' }); + } + } + } else if (node.type === 'import_from_statement') { + const moduleNameNode = node.childForFieldName('module_name'); + if (moduleNameNode) { + const moduleName = moduleNameNode.text; + const targetId = this.graphService.ensureModuleNode(moduleName); + this.graphService.addEdge({ source: filePath, target: targetId, type: 'imports' }); + } + } + + return newScope; + } + + private _getDocstring(node: SyntaxNode): string { + if (node.type === 'function_definition' || node.type === 'class_definition') { + const body = node.childForFieldName('body'); + if (body && body.namedChildCount > 0) { + const firstChild = body.namedChildren[0]; + if (firstChild.type === 'expression_statement' && firstChild.firstChild?.type === 'string') { + return firstChild.firstChild.text; + } + } + } + return ''; + } + + private _pyCalleeName(callNode: SyntaxNode): string | null { + const fn = callNode.childForFieldName('function'); + if (!fn) { + return null; + } + return this._pyRightmostName(fn); + } + + private _pyRightmostName(node: SyntaxNode): string | null { + if (node.type === 'identifier') { + return node.text; + } + + if (node.type === 'attribute') { + const attr = node.childForFieldName('attribute'); + if (attr) { + return attr.text; + } + } + + if (node.namedChildCount > 0) { + for (let i = node.namedChildCount - 1; i >= 0; i--) { + const name = this._pyRightmostName(node.namedChildren[i]); + if (name) { + return name; + } + } + } + return null; + } +} diff --git a/mcp-server/src/codemaps/parsers/typescript_parser.ts b/mcp-server/src/codemaps/parsers/typescript_parser.ts new file mode 100644 index 0000000..5135604 --- /dev/null +++ b/mcp-server/src/codemaps/parsers/typescript_parser.ts @@ -0,0 +1,383 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SyntaxNode } from 'web-tree-sitter'; +import { GraphService } from '../graph_service.js'; +import { GraphNode, GraphEdge } from '../models.js'; +import { LanguageParser } from './base_parser.js'; + +export class TypeScriptParser implements LanguageParser { + constructor(public graphService: GraphService) {} + + parse(node: SyntaxNode, filePath: string, scope: string): string { + let newScope = scope; + + if (node.type === 'function_declaration') { + const nameNode = node.childForFieldName('name'); + if (nameNode) { + const name = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const documentation = this._getDocstring(node); + const codeSnippet = node.text; + const nodeId = `${filePath}:${name}`; + this.graphService.addNode({ + id: nodeId, + type: 'function', + name, + startLine, + endLine, + documentation, + codeSnippet, + }); + newScope = nodeId; + if (scope) { + this.graphService.addEdge({ source: scope, target: nodeId, type: 'contains' }); + } + } + } else if (node.type === 'class_declaration') { + const nameNode = node.childForFieldName('name'); + if (nameNode) { + const name = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const documentation = this._getDocstring(node); + const codeSnippet = node.text; + const nodeId = `${filePath}:${name}`; + + this.graphService.addNode({ + id: nodeId, + type: 'class', + name, + startLine, + endLine, + documentation, + codeSnippet, + }); + newScope = nodeId; + if (scope) { + this.graphService.addEdge({ source: scope, target: nodeId, type: 'contains' }); + } + + const superclass = node.childForFieldName('superclass'); + let baseName: string | null = null; + if (superclass) { + baseName = this._tsRightmostIdentifier(superclass); + } + + if (!baseName) { + for (const ch of node.children) { + if ( + ch.type === 'extends_clause' || + ch.type === 'class_heritage' || + ch.type === 'heritage_clause' + ) { + const cand = this._tsRightmostIdentifier(ch); + if (cand) { + baseName = cand; + break; + } + } + } + } + + if (baseName) { + const parentNode = this.graphService.querySymbol(baseName, filePath); + if (parentNode) { + this.graphService.addEdge({ source: nodeId, target: parentNode.id, type: 'inherits' }); + } + } + + for (const ch of node.children) { + if (ch.type === 'implements_clause') { + for (const t of ch.namedChildren) { + const iface = this._tsRightmostIdentifier(t); + if (iface) { + const ifaceNode = this.graphService.querySymbol(iface, filePath); + if (ifaceNode) { + this.graphService.addEdge({ + source: nodeId, + target: ifaceNode.id, + type: 'implements', + }); + } + } + } + } + } + } + } else if (node.type === 'interface_declaration') { + const nameNode = node.childForFieldName('name'); + if (nameNode) { + const name = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const documentation = this._getDocstring(node); + const codeSnippet = node.text; + const nodeId = `${filePath}:${name}`; + this.graphService.addNode({ + id: nodeId, + type: 'interface', + name, + startLine, + endLine, + documentation, + codeSnippet, + }); + newScope = nodeId; + if (scope) { + this.graphService.addEdge({ source: scope, target: nodeId, type: 'contains' }); + } + } + } else if (node.type === 'enum_declaration') { + const nameNode = node.childForFieldName('name'); + if (nameNode) { + const name = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const codeSnippet = node.text; + const nodeId = `${filePath}:${name}`; + this.graphService.addNode({ + id: nodeId, + type: 'enum', + name, + startLine, + endLine, + documentation: '', + codeSnippet, + }); + if (scope) { + this.graphService.addEdge({ source: scope, target: nodeId, type: 'contains' }); + } + } + } else if (node.type === 'type_alias_declaration') { + const nameNode = node.childForFieldName('name'); + if (nameNode) { + const name = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const codeSnippet = node.text; + const nodeId = `${filePath}:${name}`; + this.graphService.addNode({ + id: nodeId, + type: 'type_alias', + name, + startLine, + endLine, + documentation: '', + codeSnippet, + }); + if (scope) { + this.graphService.addEdge({ source: scope, target: nodeId, type: 'contains' }); + } + } + } else if (node.type === 'method_definition') { + const nameNode = node.children.find( + (ch) => ch.type === 'property_identifier' || ch.type === 'private_property_identifier' + ); + if (nameNode) { + const name = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const documentation = this._getDocstring(node); + const codeSnippet = node.text; + const nodeId = `${filePath}:${name}`; + this.graphService.addNode({ + id: nodeId, + type: 'function', + name, + startLine, + endLine, + documentation, + codeSnippet, + }); + if (scope) { + this.graphService.addEdge({ source: scope, target: nodeId, type: 'contains' }); + } + newScope = nodeId; + } + } else if (node.type === 'method_signature') { + const nameNode = node.children.find( + (ch) => ch.type === 'property_identifier' || ch.type === 'private_property_identifier' + ); + if (nameNode) { + const name = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const codeSnippet = node.text; + const nodeId = `${filePath}:${name}`; + this.graphService.addNode({ + id: nodeId, + type: 'method', + name, + startLine, + endLine, + documentation: '', + codeSnippet, + }); + if (scope) { + this.graphService.addEdge({ source: scope, target: nodeId, type: 'contains' }); + } + } + } else if (node.type === 'variable_declarator') { + const nameNode = node.childForFieldName('name'); + const valueNode = node.childForFieldName('value'); + + if (nameNode && nameNode.type === 'identifier') { + const varName = nameNode.text; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const codeSnippet = node.text; + const varId = `${filePath}:${varName}`; + this.graphService.addNode({ + id: varId, + type: 'variable', + name: varName, + startLine, + endLine, + documentation: '', + codeSnippet, + }); + if (scope) { + this.graphService.addEdge({ source: scope, target: varId, type: 'contains' }); + } + } + + if ( + nameNode && + nameNode.type === 'identifier' && + valueNode && + (valueNode.type === 'function_expression' || valueNode.type === 'arrow_function') + ) { + const funcName = nameNode.text; + const startLine = valueNode.startPosition.row + 1; + const endLine = valueNode.endPosition.row + 1; + const documentation = this._getDocstring(node); + const codeSnippet = valueNode.text; + const funcId = `${filePath}:${funcName}`; + this.graphService.addNode({ + id: funcId, + type: 'function', + name: funcName, + startLine, + endLine, + documentation, + codeSnippet, + }); + if (scope) { + this.graphService.addEdge({ source: scope, target: funcId, type: 'contains' }); + } + } + } else if (node.type === 'call_expression') { + if (this._maybeAddCommonJsImport(node, filePath)) { + return newScope; + } + + const calleeName = this._getCalleeName(node); + if (calleeName && scope) { + const calleeNode = this.graphService.querySymbol(calleeName, filePath); + if (calleeNode) { + this.graphService.addEdge({ source: scope, target: calleeNode.id, type: 'calls' }); + } else { + this.graphService.addPendingCall(filePath, scope, calleeName); + } + } + } else if (node.type === 'import_statement') { + const sourceNode = node.childForFieldName('source'); + if (sourceNode) { + let moduleName = sourceNode.text; + if (moduleName.length >= 2 && (moduleName.startsWith("'") || moduleName.startsWith('"'))) { + moduleName = moduleName.slice(1, -1); + } + const targetId = this.graphService.ensureModuleNode(moduleName); + this.graphService.addEdge({ source: filePath, target: targetId, type: 'imports' }); + } + } + + return newScope; + } + + private _getDocstring(node: SyntaxNode): string { + const prev = node.previousSibling; + if (prev && prev.type === 'comment' && prev.text.startsWith('/**')) { + return prev.text; + } + return ''; + } + + private _getCalleeName(callNode: SyntaxNode): string | null { + const fn = callNode.childForFieldName('function'); + if (!fn) { + return null; + } + if (fn.type === 'identifier') { + return fn.text; + } + if (fn.type === 'member_expression') { + const prop = fn.childForFieldName('property'); + if (prop) { + return prop.text; + } + return this._tsRightmostIdentifier(fn); + } + return null; + } + + private _tsRightmostIdentifier(node: SyntaxNode): string | null { + if ( + node.type === 'identifier' || + node.type === 'type_identifier' || + node.type === 'property_identifier' || + node.type === 'private_property_identifier' + ) { + return node.text; + } + if (node.type === 'member_expression') { + const prop = node.childForFieldName('property'); + if (prop) { + return prop.text; + } + } + if (node.namedChildCount > 0) { + for (let i = node.namedChildCount - 1; i >= 0; i--) { + const name = this._tsRightmostIdentifier(node.namedChildren[i]); + if (name) { + return name; + } + } + } + return null; + } + + private _maybeAddCommonJsImport(callNode: SyntaxNode, filePath: string): boolean { + const fn = callNode.childForFieldName('function'); + if (!fn || fn.type !== 'identifier' || fn.text !== 'require') { + return false; + } + const args = callNode.childForFieldName('arguments'); + if (!args || args.namedChildCount === 0) { + return false; + } + const first = args.namedChildren[0]; + let mod: string | null = null; + if (first.type === 'string') { + const raw = first.text; + mod = + raw.length >= 2 && (raw.startsWith("'") || raw.startsWith('"')) ? raw.slice(1, -1) : raw; + } else if (first.type === 'template_string') { + const raw = first.text; + if (!raw.includes('${')) { + mod = raw.length >= 2 && raw.startsWith('`') && raw.endsWith('`') ? raw.slice(1, -1) : raw; + } + } + if (mod === null) { + return false; + } + const targetId = this.graphService.ensureModuleNode(mod); + this.graphService.addEdge({ source: filePath, target: targetId, type: 'imports' }); + return true; + } +} diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts index 4e85832..3ee85f9 100644 --- a/mcp-server/src/index.ts +++ b/mcp-server/src/index.ts @@ -13,7 +13,7 @@ import { promises as fs } from 'fs'; import path from 'path'; import { getAuditScope } from './filesystem.js'; import { findLineNumbers } from './security.js'; - +import { GraphBuilder, GraphService } from './codemaps/index.js'; import { runPoc } from './poc.js'; const server = new McpServer({ @@ -21,6 +21,74 @@ const server = new McpServer({ version: '0.1.0', }); +const SUPPORTED_EXTS = ['.py', '.js', '.ts', 'go']; +const DEFAULT_EXCLUDES = ['.git', 'node_modules', 'dist', 'build', 'venv', '__pycache__']; + +async function scan_dir(dir_path: string, excludes = DEFAULT_EXCLUDES, exts = SUPPORTED_EXTS) { + const files: string[] = []; + + async function scan(currentPath: string) { + const entries = await fs.readdir(currentPath, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(currentPath, entry.name); + if (excludes.includes(entry.name)) { + continue; + } + if (entry.isDirectory()) { + await scan(fullPath); + } else if (exts.some(ext => entry.name.endsWith(ext))) { + files.push(fullPath); + } + } + } + + await scan(dir_path); + return files; +} + +const graphService = new GraphService(); +const graphBuilder = new GraphBuilder(graphService); +let graphBuilt = false; + +server.tool( + 'get_enclosing_entity', + 'Get the nearest enclosing node (function/class) details (name, type, range).', + { + filePath: z.string().describe('The path to the file.'), + line: z.number().describe('The line number.'), + } as any, + async (input: any) => { + const { filePath, line } = input as { filePath: string; line: number }; + if (!graphBuilt) { + const files = await scan_dir(process.cwd()); + for (const file of files) { + await graphBuilder.buildGraph(file); + } + graphBuilt = true; + } + const entity = graphService.findEnclosingEntity(filePath, line); + if (entity) { + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(entity, null, 2), + }, + ], + }; + } else { + return { + content: [ + { + type: 'text' as const, + text: 'No enclosing entity found.', + }, + ], + }; + } + } +); + server.tool( 'find_line_numbers', 'Finds the line numbers of a code snippet in a file.', From e4344643bfebab80dbf66fef38b60ce88db4c526 Mon Sep 17 00:00:00 2001 From: Satvik Bhatnagar Date: Sun, 25 Jan 2026 22:39:47 -0800 Subject: [PATCH 02/11] Instruction updates to use Get Enclosing Entity --- commands/security/analyze-new.toml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/commands/security/analyze-new.toml b/commands/security/analyze-new.toml index 5cd3db4..002fa80 100644 --- a/commands/security/analyze-new.toml +++ b/commands/security/analyze-new.toml @@ -94,10 +94,13 @@ You will now begin executing the plan. The following are your precise instructio * If the above command fails, producing a fatal error: then proceed to step 1b. 1a. **To define the audit scope in a git repository** - * You **MUST** run the exact command: `git diff -U10 --merge-base origin/HEAD | grep -v '^-'`. - * If this command fails and does not produce a changelist, use this exact command: `git diff -U10 | grep -v '^-'`. - * This is your only method for determining the diff. Do not use any other commands for this purpose. - * Once the command is executed and you have the diff, you will mark this task as complete. + * To get the code changes, you **MUST** run `git diff --unified=0 --merge-base origin/HEAD | grep '^\+'`. + * If this command fails, use `git diff --unified=0 | grep '^\+'`. This will give you only the added lines. + * For each added line, you need to get its context. To do this, you **MUST** parse the output to find the file path and the line number of the added line. + * Then, for each added line, you **MUST** use the `get_enclosing_entity` function with the file path and line number. This will give you the full source code of the function or class that contains the new code. + * The combination of the added lines and their enclosing entity is the scope of your audit for that change. + * This is your only method for determining the diff and its context. Do not use any other commands for this purpose. + * Once you have the enclosing entity for all added code blocks, you will mark this task as complete. 1b. **To define the audit scope in a non-git folder** * Let the user know that you were unable to generate an automatic changelist with git, so you **MUST** prompt the user for files to security scan. From eff6019df46c6a84593bf41de12201695e54daed Mon Sep 17 00:00:00 2001 From: Satvik Bhatnagar Date: Wed, 28 Jan 2026 18:40:56 -0800 Subject: [PATCH 03/11] Refactored and In Working State --- .gitignore | 2 +- commands/security/analyze-new.toml | 19 ++-- mcp-server/package-lock.json | 93 +++++++++++++++++-- mcp-server/package.json | 5 +- mcp-server/src/codemaps/graph_builder.ts | 54 ++++++----- .../src/codemaps/parsers/base_parser.ts | 2 +- mcp-server/src/codemaps/parsers/go_parser.ts | 2 +- .../src/codemaps/parsers/javascript_parser.ts | 2 +- .../src/codemaps/parsers/python_parser.ts | 2 +- .../src/codemaps/parsers/typescript_parser.ts | 2 +- mcp-server/src/index.ts | 72 ++++++++++---- 11 files changed, 188 insertions(+), 67 deletions(-) diff --git a/.gitignore b/.gitignore index 81340bf..bc43492 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ .env dist/ -node_modules/ \ No newline at end of file +node_modules/.DS_Store \ No newline at end of file diff --git a/commands/security/analyze-new.toml b/commands/security/analyze-new.toml index 002fa80..bc3b47f 100644 --- a/commands/security/analyze-new.toml +++ b/commands/security/analyze-new.toml @@ -57,7 +57,7 @@ For EVERY task, you MUST follow this procedure. 5. **Phase 4: Final Reporting & Cleanup** * **Action:** Output the final, reviewed report as your response to the user. * **Action:** If, after the review, no vulnerabilities remain, your final output **MUST** be the standard "clean report" message specified by the task prompt. - * **Action:** Remove the temporary files (`SECURITY_ANALYSIS_TODO.md` and `DRAFT_SECURITY_REPORT.md`) from the `.gemini_security/` directory unless instructed otherwise. Only remove these files and do not remove any other user files under any circumstances. + * **Action:** Remove the temporary files (`SECURITY_ANALYSIS_TODO.md` and `DRAFT_SECURITY_REPORT.md`) from the `.gemini_security_new/` directory unless instructed otherwise. Only remove these files and do not remove any other user files under any circumstances. ### Example of the Workflow in `SECURITY_ANALYSIS_TODO.md` @@ -94,13 +94,18 @@ You will now begin executing the plan. The following are your precise instructio * If the above command fails, producing a fatal error: then proceed to step 1b. 1a. **To define the audit scope in a git repository** - * To get the code changes, you **MUST** run `git diff --unified=0 --merge-base origin/HEAD | grep '^\+'`. - * If this command fails, use `git diff --unified=0 | grep '^\+'`. This will give you only the added lines. - * For each added line, you need to get its context. To do this, you **MUST** parse the output to find the file path and the line number of the added line. - * Then, for each added line, you **MUST** use the `get_enclosing_entity` function with the file path and line number. This will give you the full source code of the function or class that contains the new code. - * The combination of the added lines and their enclosing entity is the scope of your audit for that change. + * To get the added code, you **MUST** run `git diff --unified=0 --merge-base origin/HEAD | grep '^\\+'`. This command provides a simple list of all added lines. + * From this list, parse the file path and the **text content** for each added line. + * **Optimized Context Retrieval:** To efficiently get the context for these changes, you must avoid redundant tool calls. You will maintain a record of the functions you have already retrieved for each file. + 1. Iterate through your list of changed lines (`file_path`, `line_content`). For each one, first use the `find_line_numbers` tool to get its precise `line_number`. + 2. Before calling the next tool, check if this `line_number` is already covered by a function you have previously retrieved and recorded for that file. + 3. If it is **not** covered, you **MUST** then call `get_enclosing_entity` and record the retrieved function and its line range. + 4. If it **is** covered, you **MUST NOT** call `get_enclosing_entity` again. + * This procedure ensures you only call the tools for the minimum number of lines necessary to identify each unique, changed function. + * The collection of unique enclosing entities you retrieve is the scope of your audit. + * **Fallback Mechanism:** If the primary workflow fails for any change block (e.g., `find_line_numbers` cannot locate the snippet, or `get_enclosing_entity` returns an error), you **MUST** fall back to a context-based diff for that specific block. Run `git diff -U10 --merge-base origin/HEAD | grep -v '^-'` and use the resulting hunk (the changed lines plus 10 lines of context before and after) as the scope for your analysis of that specific change. * This is your only method for determining the diff and its context. Do not use any other commands for this purpose. - * Once you have the enclosing entity for all added code blocks, you will mark this task as complete. + * Once you have the enclosing entities for all added code blocks, you will mark this task as complete. 1b. **To define the audit scope in a non-git folder** * Let the user know that you were unable to generate an automatic changelist with git, so you **MUST** prompt the user for files to security scan. diff --git a/mcp-server/package-lock.json b/mcp-server/package-lock.json index ab9b958..4d4f47e 100644 --- a/mcp-server/package-lock.json +++ b/mcp-server/package-lock.json @@ -8,7 +8,10 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.24.0", "tree-sitter": "^0.21.0", - "web-tree-sitter": "^0.22.0", + "tree-sitter-go": "^0.21.0", + "tree-sitter-javascript": "^0.21.0", + "tree-sitter-python": "^0.21.0", + "tree-sitter-typescript": "^0.21.0", "zod": "^3.24.2" }, "devDependencies": { @@ -2391,6 +2394,88 @@ "node-gyp-build": "^4.8.0" } }, + "node_modules/tree-sitter-go": { + "version": "0.21.2", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/tree-sitter-go/-/tree-sitter-go-0.21.2.tgz", + "integrity": "sha512-aMFwjsB948nWhURiIxExK8QX29JYKs96P/IfXVvluVMRJZpL04SREHsdOZHYqJr1whkb7zr3/gWHqqvlkczmvw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.1.0", + "node-gyp-build": "^4.8.1" + }, + "peerDependencies": { + "tree-sitter": "^0.21.0" + }, + "peerDependenciesMeta": { + "tree_sitter": { + "optional": true + } + } + }, + "node_modules/tree-sitter-javascript": { + "version": "0.21.4", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/tree-sitter-javascript/-/tree-sitter-javascript-0.21.4.tgz", + "integrity": "sha512-Lrk8yahebwrwc1sWJE9xPcz1OnnqiEV7Dh5fbN6EN3wNAdu9r06HpTqLqDwUUbnG4EB46Sfk+FJFAOldfoKLOw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.0.0", + "node-gyp-build": "^4.8.1" + }, + "peerDependencies": { + "tree-sitter": "^0.21.1" + }, + "peerDependenciesMeta": { + "tree_sitter": { + "optional": true + } + } + }, + "node_modules/tree-sitter-python": { + "version": "0.21.0", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/tree-sitter-python/-/tree-sitter-python-0.21.0.tgz", + "integrity": "sha512-IUKx7JcTVbByUx1iHGFS/QsIjx7pqwTMHL9bl/NGyhyyydbfNrpruo2C7W6V4KZrbkkCOlX8QVrCoGOFW5qecg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.1.0", + "node-gyp-build": "^4.8.0" + }, + "peerDependencies": { + "tree-sitter": "^0.21.0" + }, + "peerDependenciesMeta": { + "tree_sitter": { + "optional": true + } + } + }, + "node_modules/tree-sitter-python/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/tree-sitter-typescript": { + "version": "0.21.2", + "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/tree-sitter-typescript/-/tree-sitter-typescript-0.21.2.tgz", + "integrity": "sha512-/RyNK41ZpkA8PuPZimR6pGLvNR1p0ibRUJwwQn4qAjyyLEIQD/BNlwS3NSxWtGsAWZe9gZ44VK1mWx2+eQVldg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.0.0", + "node-gyp-build": "^4.8.1" + }, + "peerDependencies": { + "tree-sitter": "^0.21.0" + }, + "peerDependenciesMeta": { + "tree_sitter": { + "optional": true + } + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -2615,12 +2700,6 @@ } } }, - "node_modules/web-tree-sitter": { - "version": "0.22.6", - "resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/web-tree-sitter/-/web-tree-sitter-0.22.6.tgz", - "integrity": "sha512-hS87TH71Zd6mGAmYCvlgxeGDjqd9GTeqXNqTT+u0Gs51uIozNIaaq/kUAbV/Zf56jb2ZOyG8BxZs2GG9wbLi6Q==", - "license": "MIT" - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/mcp-server/package.json b/mcp-server/package.json index 1a6c07a..355fb33 100644 --- a/mcp-server/package.json +++ b/mcp-server/package.json @@ -19,6 +19,9 @@ "@modelcontextprotocol/sdk": "^1.24.0", "zod": "^3.24.2", "tree-sitter": "^0.21.0", - "web-tree-sitter": "^0.22.0" + "tree-sitter-python": "^0.21.0", + "tree-sitter-javascript": "^0.21.0", + "tree-sitter-go": "^0.21.0", + "tree-sitter-typescript": "^0.21.0" } } \ No newline at end of file diff --git a/mcp-server/src/codemaps/graph_builder.ts b/mcp-server/src/codemaps/graph_builder.ts index 7535138..2862149 100644 --- a/mcp-server/src/codemaps/graph_builder.ts +++ b/mcp-server/src/codemaps/graph_builder.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import Parser from 'web-tree-sitter'; +import Parser from 'tree-sitter'; import { GraphService } from './graph_service.js'; import { GraphNode } from './models.js'; import { PythonParser } from './parsers/python_parser.js'; @@ -13,42 +13,42 @@ import { GoParser } from './parsers/go_parser.js'; import { TypeScriptParser } from './parsers/typescript_parser.js'; import { LanguageParser } from './parsers/base_parser.js'; import { promises as fs } from 'fs'; +import Python from 'tree-sitter-python'; +import JavaScript from 'tree-sitter-javascript'; +import Go from 'tree-sitter-go'; +import TypeScript from 'tree-sitter-typescript'; export class GraphBuilder { private parser!: Parser; private languageParsers: { [key: string]: LanguageParser }; - private languageLibs: { [key: string]: string }; - - constructor(private graphService: GraphService) { - this.languageParsers = { - python: new PythonParser(graphService), - javascript: new JavaScriptParser(graphService), - go: new GoParser(graphService), - typescript: new TypeScriptParser(graphService), - }; - this.languageLibs = { - python: 'tree-sitter-python.wasm', - javascript: 'tree-sitter-javascript.wasm', - go: 'tree-sitter-go.wasm', - typescript: 'tree-sitter-typescript.wasm', - }; - } + private languages: { [key: string]: object }; + constructor(private graphService: GraphService) { + this.languageParsers = { + python: new PythonParser(graphService), + javascript: new JavaScriptParser(graphService), + go: new GoParser(graphService), + typescript: new TypeScriptParser(graphService), + }; + this.languages = { + python: Python, + javascript: JavaScript, + go: Go, + typescript: TypeScript, + }; + } - public async buildGraph(filePath: string) { - const language = this._getLanguageFromFileExtension(filePath); - const languageLibPath = this.languageLibs[language]; - if (!languageLibPath) { + public async buildGraph(filePath: string) { + const language = this._getLanguageFromFileExtension(filePath); + const languageMapping = this.languages[language]; + if (!languageMapping) { throw new Error(`Unsupported language: ${language}`); } - - await Parser.init(); - const Lang = await Parser.Language.load(languageLibPath); + this.parser = new Parser(); - this.parser.setLanguage(Lang); + this.parser.setLanguage(languageMapping); const fileContent = await fs.readFile(filePath, 'utf8'); const tree = this.parser.parse(fileContent); - const fileNode: GraphNode = { id: filePath, type: 'file', @@ -59,10 +59,8 @@ export class GraphBuilder { codeSnippet: '', }; this.graphService.addNode(fileNode); - const languageParser = this.languageParsers[language]; this._traverseTree(tree.rootNode, languageParser, filePath, filePath); - return this.graphService.graph; } diff --git a/mcp-server/src/codemaps/parsers/base_parser.ts b/mcp-server/src/codemaps/parsers/base_parser.ts index 0fa2639..e81521c 100644 --- a/mcp-server/src/codemaps/parsers/base_parser.ts +++ b/mcp-server/src/codemaps/parsers/base_parser.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SyntaxNode } from 'web-tree-sitter'; +import { SyntaxNode } from 'tree-sitter'; import { GraphService } from '../graph_service.js'; export interface LanguageParser { diff --git a/mcp-server/src/codemaps/parsers/go_parser.ts b/mcp-server/src/codemaps/parsers/go_parser.ts index 2064e2a..7696c27 100644 --- a/mcp-server/src/codemaps/parsers/go_parser.ts +++ b/mcp-server/src/codemaps/parsers/go_parser.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SyntaxNode } from 'web-tree-sitter'; +import { SyntaxNode } from 'tree-sitter'; import { GraphService } from '../graph_service.js'; import { GraphNode, GraphEdge } from '../models.js'; import { LanguageParser } from './base_parser.js'; diff --git a/mcp-server/src/codemaps/parsers/javascript_parser.ts b/mcp-server/src/codemaps/parsers/javascript_parser.ts index 9cf9c09..af68a14 100644 --- a/mcp-server/src/codemaps/parsers/javascript_parser.ts +++ b/mcp-server/src/codemaps/parsers/javascript_parser.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SyntaxNode } from 'web-tree-sitter'; +import { SyntaxNode } from 'tree-sitter'; import { GraphService } from '../graph_service.js'; import { GraphNode, GraphEdge } from '../models.js'; import { LanguageParser } from './base_parser.js'; diff --git a/mcp-server/src/codemaps/parsers/python_parser.ts b/mcp-server/src/codemaps/parsers/python_parser.ts index 7b1c016..b9e4bee 100644 --- a/mcp-server/src/codemaps/parsers/python_parser.ts +++ b/mcp-server/src/codemaps/parsers/python_parser.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SyntaxNode } from 'web-tree-sitter'; +import { SyntaxNode } from 'tree-sitter'; import { GraphService } from '../graph_service.js'; import { GraphNode, GraphEdge } from '../models.js'; import { LanguageParser } from './base_parser.js'; diff --git a/mcp-server/src/codemaps/parsers/typescript_parser.ts b/mcp-server/src/codemaps/parsers/typescript_parser.ts index 5135604..0467743 100644 --- a/mcp-server/src/codemaps/parsers/typescript_parser.ts +++ b/mcp-server/src/codemaps/parsers/typescript_parser.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SyntaxNode } from 'web-tree-sitter'; +import { SyntaxNode } from 'tree-sitter'; import { GraphService } from '../graph_service.js'; import { GraphNode, GraphEdge } from '../models.js'; import { LanguageParser } from './base_parser.js'; diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts index 3ee85f9..3576526 100644 --- a/mcp-server/src/index.ts +++ b/mcp-server/src/index.ts @@ -11,6 +11,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { z } from 'zod'; import { promises as fs } from 'fs'; import path from 'path'; +import os from 'os'; import { getAuditScope } from './filesystem.js'; import { findLineNumbers } from './security.js'; import { GraphBuilder, GraphService } from './codemaps/index.js'; @@ -54,21 +55,52 @@ server.tool( 'get_enclosing_entity', 'Get the nearest enclosing node (function/class) details (name, type, range).', { - filePath: z.string().describe('The path to the file.'), + file_path: z.string().describe('The path to the file.'), line: z.number().describe('The line number.'), } as any, async (input: any) => { - const { filePath, line } = input as { filePath: string; line: number }; - if (!graphBuilt) { - const files = await scan_dir(process.cwd()); - for (const file of files) { + const logFilePath = path.join(os.homedir(), 'Desktop', 'satvikkk-Github', 'security-gCLI', 'debug.log'); + await fs.appendFile(logFilePath, `Received input: ${JSON.stringify(input, null, 2)}\n`); + + // The first call can be empty, so we guard against it. + if (!input.file_path) { + return { + content: [{ type: 'text', text: 'Invalid argument: file_path is missing.' }], + }; + } + + const { file_path, line } = input as { file_path: string; line: number }; + + // Sanitize and resolve the file path to be absolute + let sanitizedFilePath = file_path.trim(); + if (sanitizedFilePath.startsWith('"') && sanitizedFilePath.endsWith('"')) { + sanitizedFilePath = sanitizedFilePath.substring(1, sanitizedFilePath.length - 1); + } + if (sanitizedFilePath.startsWith('a/')) { + sanitizedFilePath = sanitizedFilePath.substring(2); + } else if (sanitizedFilePath.startsWith('b/')) { + sanitizedFilePath = sanitizedFilePath.substring(2); + } + + const absoluteFilePath = path.resolve(process.cwd(), sanitizedFilePath); + await fs.appendFile(logFilePath, `Absolute file path: ${absoluteFilePath}\n`); + + // Always rebuild the graph to ensure we are using the correct project context. + const files = await scan_dir(process.cwd()); + await fs.appendFile(logFilePath, `Scanning files for graph build: ${JSON.stringify(files, null, 2)}\n`); + for (const file of files) { + try { await graphBuilder.buildGraph(file); + } catch (e: any) { + await fs.appendFile(logFilePath, `Error building graph for file ${file}: ${e.message}\n`); } - graphBuilt = true; } - const entity = graphService.findEnclosingEntity(filePath, line); + + const entity = graphService.findEnclosingEntity(absoluteFilePath, line); + await fs.appendFile(logFilePath, `Found entity: ${JSON.stringify(entity, null, 2)}\n`); + if (entity) { - return { + const response = { content: [ { type: 'text' as const, @@ -76,15 +108,19 @@ server.tool( }, ], }; + await fs.appendFile(logFilePath, `Returning response: ${JSON.stringify(response, null, 2)}\n`); + return response as any; } else { - return { - content: [ - { - type: 'text' as const, - text: 'No enclosing entity found.', - }, - ], - }; + const response = { + content: [ + { + type: 'text' as const, + text: 'No enclosing entity found.', + }, + ], + }; + await fs.appendFile(logFilePath, `Returning response: ${JSON.stringify(response, null, 2)}\n`); + return response as any; } } ); @@ -112,11 +148,11 @@ server.tool( return { content: [ { - type: 'text', + type: "text", text: diff, }, ], - }; + } as any; } ); From 210ff138b93fe5ee7bea39dc537f2f501acb1031 Mon Sep 17 00:00:00 2001 From: Satvik Bhatnagar Date: Wed, 28 Jan 2026 18:41:53 -0800 Subject: [PATCH 04/11] Modify Git Ignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index bc43492..3fbde00 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .env dist/ -node_modules/.DS_Store \ No newline at end of file +node_modules/ +.DS_Store \ No newline at end of file From 38d988bc55ab54ad5d565890322a653f5f315c90 Mon Sep 17 00:00:00 2001 From: Satvik Bhatnagar Date: Fri, 30 Jan 2026 16:30:27 -0800 Subject: [PATCH 05/11] Remove debugging --- mcp-server/src/index.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts index 3576526..dd0dca0 100644 --- a/mcp-server/src/index.ts +++ b/mcp-server/src/index.ts @@ -59,8 +59,6 @@ server.tool( line: z.number().describe('The line number.'), } as any, async (input: any) => { - const logFilePath = path.join(os.homedir(), 'Desktop', 'satvikkk-Github', 'security-gCLI', 'debug.log'); - await fs.appendFile(logFilePath, `Received input: ${JSON.stringify(input, null, 2)}\n`); // The first call can be empty, so we guard against it. if (!input.file_path) { @@ -83,21 +81,17 @@ server.tool( } const absoluteFilePath = path.resolve(process.cwd(), sanitizedFilePath); - await fs.appendFile(logFilePath, `Absolute file path: ${absoluteFilePath}\n`); // Always rebuild the graph to ensure we are using the correct project context. const files = await scan_dir(process.cwd()); - await fs.appendFile(logFilePath, `Scanning files for graph build: ${JSON.stringify(files, null, 2)}\n`); for (const file of files) { try { await graphBuilder.buildGraph(file); } catch (e: any) { - await fs.appendFile(logFilePath, `Error building graph for file ${file}: ${e.message}\n`); } } const entity = graphService.findEnclosingEntity(absoluteFilePath, line); - await fs.appendFile(logFilePath, `Found entity: ${JSON.stringify(entity, null, 2)}\n`); if (entity) { const response = { @@ -108,7 +102,6 @@ server.tool( }, ], }; - await fs.appendFile(logFilePath, `Returning response: ${JSON.stringify(response, null, 2)}\n`); return response as any; } else { const response = { @@ -119,7 +112,6 @@ server.tool( }, ], }; - await fs.appendFile(logFilePath, `Returning response: ${JSON.stringify(response, null, 2)}\n`); return response as any; } } From fde4deac275c38b65d8cc3c1570488a30af09d3c Mon Sep 17 00:00:00 2001 From: Satvik Bhatnagar Date: Mon, 2 Feb 2026 14:01:57 -0800 Subject: [PATCH 06/11] Reconciling with Get Audit Scope and other instructional changes --- commands/security/analyze-new.toml | 35 +++++++++++++++--------------- mcp-server/src/filesystem.ts | 10 +++++++-- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/commands/security/analyze-new.toml b/commands/security/analyze-new.toml index bc3b47f..bf82608 100644 --- a/commands/security/analyze-new.toml +++ b/commands/security/analyze-new.toml @@ -14,18 +14,18 @@ The core principle is to trace untrusted data from its entry point (**Source**) This workflow focuses on analyzing the diff of a pull request to identify potential security risks efficiently. #### Step 1: Retrieve PR Diff -Instead of just getting a list of changed files, the workflow will retrieve the full diff for the pull request. The diff shows the specific lines that were added, modified, or removed. +Instead of just getting a list of changed files, you will retrieve the full diff for the pull request. The diff shows the specific lines that were added, modified, or removed. #### Step 2: Analyze Diff for Taint Sources -The agent will analyze only the diff content, which is significantly smaller than the full content of all changed files. -It will focus specifically on newly added or modified lines (lines prefixed with +) to identify patterns that introduce a potential "source" of untrusted input (e.g., handling of req.query, req.body, file uploads, or other external data). +You will analyze only the diff content, which is significantly smaller than the full content of all changed files. +You will focus specifically on newly added or modified lines (lines prefixed with +) to identify patterns that introduce a potential "source" of untrusted input (e.g., handling of req.query, req.body, file uploads, or other external data). #### Step 3: Build a Targeted Investigation Plan -A file will be added to the SECURITY_ANALYSIS_TODO.md for a full, deep-dive investigation only if its diff contains a potential taint source. -Files with benign changes (e.g., comment updates, dependency bumps in lockfiles, documentation changes) will be ignored entirely. Their content will never be read or processed by the agent. +You will add a file to the SECURITY_ANALYSIS_TODO.md for a full, deep-dive investigation only if its diff contains a potential taint source. +You will ignore files with benign changes (e.g., comment updates, dependency bumps in lockfiles, documentation changes) entirely. You will never read or process their content. #### Step 4: Perform Deep-Dive Investigation -The agent proceeds with its deep-dive analysis as before, but only on the much smaller, pre-qualified list of files that have been identified as containing legitimate security risks. +You will proceed with your deep-dive analysis as before, but only on the much smaller, pre-qualified list of files that have been identified as containing legitimate security risks. This diff-first approach ensures that the most expensive part of the process—reading and analyzing entire files—is reserved for the few files that actually require it. For EVERY task, you MUST follow this procedure. @@ -65,13 +65,13 @@ For EVERY task, you MUST follow this procedure. ```markdown - [ ] Define the audit scope. ``` -2. **After Scope Definition (Diff Analysis):** The agent gets the diff and finds `+ const userId = req.query.id;` in `userController.js`. It rewrites `SECURITY_ANALYSIS_TODO.md`: +2. **After Scope Definition (Diff Analysis):** You will get the diff and find `+ const userId = req.query.id;` in `userController.js`. You will then rewrite `SECURITY_ANALYSIS_TODO.md`: ```markdown - [x] Define the audit scope. - [ ] Analyze diff for taint sources and create investigation plan. - [ ] Investigate data flow from `userId` in `userController.js`. ``` -3. **Investigation Pass Begins:** The model now executes the sub-task. It traces `userId` and finds it is used in `db.run("SELECT * FROM users WHERE id = " + userId);`. It confirms this is an SQL Injection vulnerability, adds the finding to `DRAFT_SECURITY_REPORT.md`, and marks the task as complete. +3. **Investigation Pass Begins:** You will now execute the sub-task. You will trace `userId` and find it is used in `db.run("SELECT * FROM users WHERE id = " + userId);`. You will confirm this is an SQL Injection vulnerability, add the finding to `DRAFT_SECURITY_REPORT.md`, and mark the task as complete. ## Analysis Instructions @@ -89,13 +89,12 @@ Your first action is to create a `SECURITY_ANALYSIS_TODO.md` file with the follo You will now begin executing the plan. The following are your precise instructions to start with. 1. **To complete the 'Define the audit scope' task:** - * You **MUST** run the exact command: `git rev-parse --is-inside-work-tree`. - * If the above command succeeds, returning true: then proceed to step 1a. - * If the above command fails, producing a fatal error: then proceed to step 1b. + * You **MUST** use the `get_audit_scope` tool to get the code changes for your review. + * If the tool returns a diff, you will proceed to step 1a. + * If the tool does not return a diff (e.g., in a non-git directory or if there are no changes), you will proceed to step 1b. -1a. **To define the audit scope in a git repository** - * To get the added code, you **MUST** run `git diff --unified=0 --merge-base origin/HEAD | grep '^\\+'`. This command provides a simple list of all added lines. - * From this list, parse the file path and the **text content** for each added line. +1a. **To analyze a diff:** + * You will parse the file path and the **text content** for each added line from the diff. * **Optimized Context Retrieval:** To efficiently get the context for these changes, you must avoid redundant tool calls. You will maintain a record of the functions you have already retrieved for each file. 1. Iterate through your list of changed lines (`file_path`, `line_content`). For each one, first use the `find_line_numbers` tool to get its precise `line_number`. 2. Before calling the next tool, check if this `line_number` is already covered by a function you have previously retrieved and recorded for that file. @@ -103,13 +102,13 @@ You will now begin executing the plan. The following are your precise instructio 4. If it **is** covered, you **MUST NOT** call `get_enclosing_entity` again. * This procedure ensures you only call the tools for the minimum number of lines necessary to identify each unique, changed function. * The collection of unique enclosing entities you retrieve is the scope of your audit. - * **Fallback Mechanism:** If the primary workflow fails for any change block (e.g., `find_line_numbers` cannot locate the snippet, or `get_enclosing_entity` returns an error), you **MUST** fall back to a context-based diff for that specific block. Run `git diff -U10 --merge-base origin/HEAD | grep -v '^-'` and use the resulting hunk (the changed lines plus 10 lines of context before and after) as the scope for your analysis of that specific change. + * **Fallback Mechanism:** If the primary workflow fails for any change block (e.g., `find_line_numbers` cannot locate the snippet, or `get_enclosing_entity` returns an error), you **MUST** fall back to using the raw diff hunk for that specific block as the scope for your analysis. * This is your only method for determining the diff and its context. Do not use any other commands for this purpose. * Once you have the enclosing entities for all added code blocks, you will mark this task as complete. -1b. **To define the audit scope in a non-git folder** - * Let the user know that you were unable to generate an automatic changelist with git, so you **MUST** prompt the user for files to security scan. - * Match the users response to files in the workspace and build a list of files to analyze. +1b. **To analyze a user-provided list of files:** + * You **MUST** prompt the user for files to security scan. + * You will then match the user's response to files in the workspace and build a list of files to analyze. The full content of these files is your audit scope. * This is your only method for determining the files to analyze. Do not use any other commands for this purpose. * Once you have a list of files to analyze you will mark this task as complete. diff --git a/mcp-server/src/filesystem.ts b/mcp-server/src/filesystem.ts index d3850a1..8484408 100644 --- a/mcp-server/src/filesystem.ts +++ b/mcp-server/src/filesystem.ts @@ -32,11 +32,17 @@ export const isGitHubRepository = (): boolean => { export function getAuditScope(): string { let command = isGitHubRepository() ? 'git diff --merge-base origin/HEAD' : 'git diff'; try { - const diff = ( + const diffOutput = ( spawnSync('git', command.split(' ').slice(1), { encoding: 'utf-8', }).stdout || '' - ).trim(); + ); + + const diff = diffOutput + .split('\n') + .filter(line => line.startsWith('+') && !line.startsWith('+++')) + .join('\n') + .trim(); return diff; } catch (_error) { From eca4bb7ef5e51597ba5ee1e659975cea5f3971c3 Mon Sep 17 00:00:00 2001 From: Satvik Bhatnagar Date: Mon, 2 Feb 2026 23:22:50 -0800 Subject: [PATCH 07/11] Graph Saving as JSON --- mcp-server/src/codemaps/graph_service.ts | 37 ++++++++++++++++++++++++ mcp-server/src/filesystem.ts | 31 +++++++++++++------- mcp-server/src/index.ts | 21 ++++++++++---- 3 files changed, 72 insertions(+), 17 deletions(-) diff --git a/mcp-server/src/codemaps/graph_service.ts b/mcp-server/src/codemaps/graph_service.ts index 937ffc5..76a9a09 100644 --- a/mcp-server/src/codemaps/graph_service.ts +++ b/mcp-server/src/codemaps/graph_service.ts @@ -5,6 +5,8 @@ */ import { GraphNode, GraphEdge } from './models.js'; +import { promises as fs } from 'fs'; +import path from 'path'; export class GraphService { public graph: { @@ -120,4 +122,39 @@ export class GraphService { public addPendingCall(filePath: string, sourceId: string, calleeName: string) { this._pendingCalls.push([filePath, sourceId, calleeName]); } + + public async saveGraph(outputDir: string) { + const filePath = path.join(outputDir, 'codemap.json'); + const graphJson = { + nodes: Array.from(this.graph.nodes.values()), + edges: Array.from(this.graph.edges.values()).flat(), + }; + await fs.mkdir(outputDir, { recursive: true }); + await fs.writeFile(filePath, JSON.stringify(graphJson, null, 2)); + } + public async loadGraph(outputDir: string): Promise { + const filePath = path.join(outputDir, 'codemap.json'); + try { + const data = await fs.readFile(filePath, 'utf8'); + const graphJson = JSON.parse(data); + + this.graph.nodes.clear(); + this.graph.edges.clear(); + this.graph.inEdges.clear(); + this._byName.clear(); + this._byFileAndName.clear(); + this._pendingCalls = []; + + for (const node of graphJson.nodes) { + this.addNode(node); + } + + for (const edge of graphJson.edges) { + this.addEdge(edge); + } + return true; + } catch (error) { + return false; + } + } } diff --git a/mcp-server/src/filesystem.ts b/mcp-server/src/filesystem.ts index 8484408..a446c72 100644 --- a/mcp-server/src/filesystem.ts +++ b/mcp-server/src/filesystem.ts @@ -30,21 +30,30 @@ export const isGitHubRepository = (): boolean => { * Gets a changelist of the repository */ export function getAuditScope(): string { - let command = isGitHubRepository() ? 'git diff --merge-base origin/HEAD' : 'git diff'; + // --diff-filter=AM: Only Added or Modified files + // --unified=0: Removes context lines, showing only changed lines + const command = isGitHubRepository() + ? 'git diff --diff-filter=AM --unified=0 origin/HEAD' + : 'git diff --diff-filter=AM --unified=0'; + try { - const diffOutput = ( - spawnSync('git', command.split(' ').slice(1), { + const result = spawnSync('git', command.split(' ').slice(1), { encoding: 'utf-8', - }).stdout || '' - ); + }).stdout || ''; - const diff = diffOutput - .split('\n') - .filter(line => line.startsWith('+') && !line.startsWith('+++')) - .join('\n') - .trim(); + let currentFile = ''; + const diffLines = []; - return diff; + for (const line of result.split('\n')) { + if (line.startsWith('+++ b/')) { + currentFile = line.substring(6); + diffLines.push(`File: ${currentFile}`); + } else if (line.startsWith('+') && !line.startsWith('+++') && currentFile) { + diffLines.push(line); + } + } + + return diffLines.join('\n').trim(); } catch (_error) { return ""; } diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts index dd0dca0..3104f27 100644 --- a/mcp-server/src/index.ts +++ b/mcp-server/src/index.ts @@ -82,13 +82,22 @@ server.tool( const absoluteFilePath = path.resolve(process.cwd(), sanitizedFilePath); - // Always rebuild the graph to ensure we are using the correct project context. - const files = await scan_dir(process.cwd()); - for (const file of files) { - try { - await graphBuilder.buildGraph(file); - } catch (e: any) { + const GEMINI_SECURITY_DIR = path.join(process.cwd(), '.gemini_security_new'); + + if (!graphBuilt) { + const loaded = await graphService.loadGraph(GEMINI_SECURITY_DIR); + if (!loaded) { + const files = await scan_dir(process.cwd()); + for (const file of files) { + try { + await graphBuilder.buildGraph(file); + } catch (e: any) { + // Ignore errors for unsupported file types + } + } + await graphService.saveGraph(GEMINI_SECURITY_DIR); } + graphBuilt = true; } const entity = graphService.findEnclosingEntity(absoluteFilePath, line); From 4926ce8e06846956ddb6053aea3b455c38e97a46 Mon Sep 17 00:00:00 2001 From: Satvik Bhatnagar Date: Wed, 4 Feb 2026 10:56:02 -0800 Subject: [PATCH 08/11] Codemaps Unit Tests and Audit Scope revert --- mcp-server/src/codemaps/graph_builder.test.ts | 115 ++++++++++++++++++ mcp-server/src/codemaps/graph_builder.ts | 2 +- mcp-server/src/codemaps/graph_service.test.ts | 94 ++++++++++++++ mcp-server/src/codemaps/parsers/go_parser.ts | 53 +++++++- .../src/codemaps/parsers/javascript_parser.ts | 10 +- .../src/codemaps/parsers/python_parser.ts | 4 +- .../src/codemaps/parsers/typescript_parser.ts | 18 +-- mcp-server/src/filesystem.test.ts | 5 +- mcp-server/src/filesystem.ts | 27 +--- 9 files changed, 283 insertions(+), 45 deletions(-) create mode 100644 mcp-server/src/codemaps/graph_builder.test.ts create mode 100644 mcp-server/src/codemaps/graph_service.test.ts diff --git a/mcp-server/src/codemaps/graph_builder.test.ts b/mcp-server/src/codemaps/graph_builder.test.ts new file mode 100644 index 0000000..d8b0404 --- /dev/null +++ b/mcp-server/src/codemaps/graph_builder.test.ts @@ -0,0 +1,115 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { GraphBuilder } from './graph_builder'; +import { GraphService } from './graph_service'; +import { promises as fs } from 'fs'; + +vi.mock('fs', () => ({ + promises: { + readFile: vi.fn(), + }, +})); + +describe('GraphBuilder', () => { + let graphService: GraphService; + let graphBuilder: GraphBuilder; + + beforeEach(() => { + graphService = new GraphService(); + graphBuilder = new GraphBuilder(graphService); + }); + + it('should build a graph for a Python file', async () => { + const filePath = 'test.py'; + const fileContent = ` +def my_function(): + pass + +class MyClass: + def my_method(self): + pass +`; + vi.mocked(fs.readFile).mockResolvedValue(fileContent); + + await graphBuilder.buildGraph(filePath); + + expect(graphService.graph.nodes.has('test.py')).toBe(true); + expect(graphService.graph.nodes.has('test.py:my_function')).toBe(true); + expect(graphService.graph.nodes.has('test.py:MyClass')).toBe(true); + expect(graphService.graph.nodes.has('test.py:MyClass:my_method')).toBe(true); + }); + + it('should build a graph for a JavaScript file', async () => { + const filePath = 'test.js'; + const fileContent = ` +function myFunction() { +} + +class MyClass { + myMethod() { + } +} +`; + vi.mocked(fs.readFile).mockResolvedValue(fileContent); + + await graphBuilder.buildGraph(filePath); + + expect(graphService.graph.nodes.has('test.js')).toBe(true); + expect(graphService.graph.nodes.has('test.js:myFunction')).toBe(true); + expect(graphService.graph.nodes.has('test.js:MyClass')).toBe(true); + expect(graphService.graph.nodes.has('test.js:MyClass:myMethod')).toBe(true); + }); + + it('should build a graph for a TypeScript file', async () => { + const filePath = 'test.ts'; + const fileContent = ` +function myFunction(): void { +} + +class MyClass { + myMethod(): void { + } +} +`; + vi.mocked(fs.readFile).mockResolvedValue(fileContent); + + await graphBuilder.buildGraph(filePath); + + expect(graphService.graph.nodes.has('test.ts')).toBe(true); + expect(graphService.graph.nodes.has('test.ts:myFunction')).toBe(true); + expect(graphService.graph.nodes.has('test.ts:MyClass')).toBe(true); + expect(graphService.graph.nodes.has('test.ts:MyClass:myMethod')).toBe(true); + }); + + it('should build a graph for a Go file', async () => { + const filePath = 'test.go'; + const fileContent = ` +func myFunction() { +} + +type MyStruct struct { +} + +func (s *MyStruct) myMethod() { +} +`; + vi.mocked(fs.readFile).mockResolvedValue(fileContent); + + await graphBuilder.buildGraph(filePath); + + expect(graphService.graph.nodes.has('test.go')).toBe(true); + expect(graphService.graph.nodes.has('test.go:myFunction')).toBe(true); + expect(graphService.graph.nodes.has('test.go:MyStruct')).toBe(true); + expect(graphService.graph.nodes.has('test.go:MyStruct:myMethod')).toBe(true); + }); + + it('should throw an error for an unsupported file extension', async () => { + const filePath = 'test.txt'; + await expect(graphBuilder.buildGraph(filePath)).rejects.toThrow('Unsupported file extension: test.txt'); + }); +}); diff --git a/mcp-server/src/codemaps/graph_builder.ts b/mcp-server/src/codemaps/graph_builder.ts index 2862149..b2b91e7 100644 --- a/mcp-server/src/codemaps/graph_builder.ts +++ b/mcp-server/src/codemaps/graph_builder.ts @@ -33,7 +33,7 @@ export class GraphBuilder { python: Python, javascript: JavaScript, go: Go, - typescript: TypeScript, + typescript: TypeScript.typescript, }; } diff --git a/mcp-server/src/codemaps/graph_service.test.ts b/mcp-server/src/codemaps/graph_service.test.ts new file mode 100644 index 0000000..5a6be1d --- /dev/null +++ b/mcp-server/src/codemaps/graph_service.test.ts @@ -0,0 +1,94 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { GraphService } from './graph_service'; +import { GraphNode } from './models'; + +describe('GraphService', () => { + let graphService: GraphService; + + beforeEach(() => { + graphService = new GraphService(); + }); + + it('should add a node to the graph', () => { + const node: GraphNode = { + id: 'file.py:my_function', + type: 'function', + name: 'my_function', + startLine: 1, + endLine: 10, + documentation: '', + codeSnippet: '', + }; + graphService.addNode(node); + expect(graphService.graph.nodes.get('file.py:my_function')).toEqual(node); + }); + + it('should add an edge to the graph', () => { + const edge = { + source: 'file.py:my_function', + target: 'file.py:another_function', + type: 'calls', + }; + graphService.addEdge(edge); + expect(graphService.graph.edges.get('file.py:my_function')).toEqual([edge]); + expect(graphService.graph.inEdges.get('file.py:another_function')).toEqual([edge]); + }); + + it('should find the enclosing entity', () => { + const fileNode: GraphNode = { + id: 'file.py', + type: 'file', + name: 'file.py', + startLine: 0, + endLine: 0, + documentation: '', + codeSnippet: '', + }; + const functionNode: GraphNode = { + id: 'file.py:my_function', + type: 'function', + name: 'my_function', + startLine: 1, + endLine: 10, + documentation: '', + codeSnippet: '', + }; + const innerFunctionNode: GraphNode = { + id: 'file.py:my_function:inner', + type: 'function', + name: 'inner', + startLine: 2, + endLine: 5, + documentation: '', + codeSnippet: '', + }; + graphService.addNode(fileNode); + graphService.addNode(functionNode); + graphService.addNode(innerFunctionNode); + + const enclosingEntity = graphService.findEnclosingEntity('file.py', 3); + expect(enclosingEntity).toEqual(innerFunctionNode); + }); + + it('should query a symbol', () => { + const node: GraphNode = { + id: 'file.py:my_function', + type: 'function', + name: 'my_function', + startLine: 1, + endLine: 10, + documentation: '', + codeSnippet: '', + }; + graphService.addNode(node); + + const foundNode = graphService.querySymbol('my_function', 'file.py'); + expect(foundNode).toEqual(node); + }); +}); diff --git a/mcp-server/src/codemaps/parsers/go_parser.ts b/mcp-server/src/codemaps/parsers/go_parser.ts index 7696c27..47630a7 100644 --- a/mcp-server/src/codemaps/parsers/go_parser.ts +++ b/mcp-server/src/codemaps/parsers/go_parser.ts @@ -23,7 +23,7 @@ export class GoParser implements LanguageParser { const endLine = node.endPosition.row + 1; const documentation = this._getDocstring(node); const codeSnippet = node.text; - const nodeId = `${filePath}:${name}`; + const nodeId = `${scope}:${name}`; this.graphService.addNode({ id: nodeId, @@ -41,8 +41,37 @@ export class GoParser implements LanguageParser { } } else if (node.type === 'type_declaration') { for (const spec of this._findAll(node, 'type_spec')) { - this._handleTypeSpec(spec, filePath); + this._handleTypeSpec(spec, filePath, scope); } + } else if (node.type === 'method_declaration') { + const receiverNode = node.childForFieldName('receiver'); + const nameNode = node.childForFieldName('name'); + if (receiverNode && nameNode) { + const receiverType = this._getReceiverType(receiverNode); + const methodName = nameNode.text; + if (receiverType) { + const parentNode = this.graphService.querySymbol(receiverType, filePath); + if (parentNode) { + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + const documentation = this._getDocstring(node); + const codeSnippet = node.text; + const nodeId = `${parentNode.id}:${methodName}`; + + this.graphService.addNode({ + id: nodeId, + type: 'function', + name: methodName, + startLine, + endLine, + documentation, + codeSnippet, + }); + newScope = nodeId; + this.graphService.addEdge({ source: parentNode.id, target: nodeId, type: 'contains' }); + } + } + } } else if (node.type === 'call_expression') { const callee = this._extractCalleeName(node); if (callee && scope) { @@ -76,7 +105,7 @@ export class GoParser implements LanguageParser { return newScope; } - private _handleTypeSpec(specNode: SyntaxNode, filePath: string) { + private _handleTypeSpec(specNode: SyntaxNode, filePath: string, scope: string) { const nameNode = specNode.childForFieldName('name'); const typeNode = specNode.childForFieldName('type'); if (!nameNode || !typeNode) { @@ -91,7 +120,7 @@ export class GoParser implements LanguageParser { const startLine = specNode.startPosition.row + 1; const endLine = specNode.endPosition.row + 1; const codeSnippet = specNode.text; - const nodeId = `${filePath}:${name}`; + const nodeId = `${scope}:${name}`; this.graphService.addNode({ id: nodeId, @@ -121,8 +150,7 @@ export class GoParser implements LanguageParser { continue; } - const parentNode = this.graphService.querySymbol(parentName); - if (parentNode) { + const parentNode = this.graphService.querySymbol(parentName, filePath); if (parentNode) { this.graphService.addEdge({ source: nodeId, target: parentNode.id, type: 'inherits' }); } } @@ -227,4 +255,17 @@ export class GoParser implements LanguageParser { } return ''; } + + private _getReceiverType(node: SyntaxNode): string | null { + if (node.type === 'type_identifier') { + return node.text; + } + for (const child of node.children) { + const found = this._getReceiverType(child); + if (found) { + return found; + } + } + return null; + } } diff --git a/mcp-server/src/codemaps/parsers/javascript_parser.ts b/mcp-server/src/codemaps/parsers/javascript_parser.ts index af68a14..5e7c692 100644 --- a/mcp-server/src/codemaps/parsers/javascript_parser.ts +++ b/mcp-server/src/codemaps/parsers/javascript_parser.ts @@ -23,7 +23,7 @@ export class JavaScriptParser implements LanguageParser { const endLine = node.endPosition.row + 1; const documentation = this._getDocstring(node); const codeSnippet = node.text; - const nodeId = `${filePath}:${name}`; + const nodeId = `${scope}:${name}`; this.graphService.addNode({ id: nodeId, @@ -48,7 +48,7 @@ export class JavaScriptParser implements LanguageParser { const endLine = node.endPosition.row + 1; const documentation = this._getDocstring(node); const codeSnippet = node.text; - const nodeId = `${filePath}:${name}`; + const nodeId = `${scope}:${name}`; this.graphService.addNode({ id: nodeId, @@ -82,7 +82,7 @@ export class JavaScriptParser implements LanguageParser { const endLine = node.endPosition.row + 1; const documentation = this._getDocstring(node); const codeSnippet = node.text; - const nodeId = `${filePath}:${methodName}`; + const nodeId = `${scope}:${methodName}`; this.graphService.addNode({ id: nodeId, @@ -123,7 +123,7 @@ export class JavaScriptParser implements LanguageParser { const startLine = node.startPosition.row + 1; const endLine = node.endPosition.row + 1; const codeSnippet = node.text; - const varId = `${filePath}:${varName}`; + const varId = `${scope}:${varName}`; this.graphService.addNode({ id: varId, type: 'variable', @@ -149,7 +149,7 @@ export class JavaScriptParser implements LanguageParser { const endLine = valueNode.endPosition.row + 1; const documentation = this._getDocstring(node); const codeSnippet = valueNode.text; - const funcId = `${filePath}:${funcName}`; + const funcId = `${scope}:${funcName}`; this.graphService.addNode({ id: funcId, type: 'function', diff --git a/mcp-server/src/codemaps/parsers/python_parser.ts b/mcp-server/src/codemaps/parsers/python_parser.ts index b9e4bee..904a92c 100644 --- a/mcp-server/src/codemaps/parsers/python_parser.ts +++ b/mcp-server/src/codemaps/parsers/python_parser.ts @@ -22,7 +22,7 @@ export class PythonParser implements LanguageParser { const endLine = node.endPosition.row + 1; const documentation = this._getDocstring(node); const codeSnippet = node.text; - const nodeId = `${filePath}:${name}`; + const nodeId = `${scope}:${name}`; const newNode: GraphNode = { id: nodeId, @@ -47,7 +47,7 @@ export class PythonParser implements LanguageParser { const endLine = node.endPosition.row + 1; const documentation = this._getDocstring(node); const codeSnippet = node.text; - const nodeId = `${filePath}:${name}`; + const nodeId = `${scope}:${name}`; const newNode: GraphNode = { id: nodeId, diff --git a/mcp-server/src/codemaps/parsers/typescript_parser.ts b/mcp-server/src/codemaps/parsers/typescript_parser.ts index 0467743..dd3527c 100644 --- a/mcp-server/src/codemaps/parsers/typescript_parser.ts +++ b/mcp-server/src/codemaps/parsers/typescript_parser.ts @@ -23,7 +23,7 @@ export class TypeScriptParser implements LanguageParser { const endLine = node.endPosition.row + 1; const documentation = this._getDocstring(node); const codeSnippet = node.text; - const nodeId = `${filePath}:${name}`; + const nodeId = `${scope}:${name}`; this.graphService.addNode({ id: nodeId, type: 'function', @@ -46,7 +46,7 @@ export class TypeScriptParser implements LanguageParser { const endLine = node.endPosition.row + 1; const documentation = this._getDocstring(node); const codeSnippet = node.text; - const nodeId = `${filePath}:${name}`; + const nodeId = `${scope}:${name}`; this.graphService.addNode({ id: nodeId, @@ -117,7 +117,7 @@ export class TypeScriptParser implements LanguageParser { const endLine = node.endPosition.row + 1; const documentation = this._getDocstring(node); const codeSnippet = node.text; - const nodeId = `${filePath}:${name}`; + const nodeId = `${scope}:${name}`; this.graphService.addNode({ id: nodeId, type: 'interface', @@ -139,7 +139,7 @@ export class TypeScriptParser implements LanguageParser { const startLine = node.startPosition.row + 1; const endLine = node.endPosition.row + 1; const codeSnippet = node.text; - const nodeId = `${filePath}:${name}`; + const nodeId = `${scope}:${name}`; this.graphService.addNode({ id: nodeId, type: 'enum', @@ -160,7 +160,7 @@ export class TypeScriptParser implements LanguageParser { const startLine = node.startPosition.row + 1; const endLine = node.endPosition.row + 1; const codeSnippet = node.text; - const nodeId = `${filePath}:${name}`; + const nodeId = `${scope}:${name}`; this.graphService.addNode({ id: nodeId, type: 'type_alias', @@ -184,7 +184,7 @@ export class TypeScriptParser implements LanguageParser { const endLine = node.endPosition.row + 1; const documentation = this._getDocstring(node); const codeSnippet = node.text; - const nodeId = `${filePath}:${name}`; + const nodeId = `${scope}:${name}`; this.graphService.addNode({ id: nodeId, type: 'function', @@ -208,7 +208,7 @@ export class TypeScriptParser implements LanguageParser { const startLine = node.startPosition.row + 1; const endLine = node.endPosition.row + 1; const codeSnippet = node.text; - const nodeId = `${filePath}:${name}`; + const nodeId = `${scope}:${name}`; this.graphService.addNode({ id: nodeId, type: 'method', @@ -231,7 +231,7 @@ export class TypeScriptParser implements LanguageParser { const startLine = node.startPosition.row + 1; const endLine = node.endPosition.row + 1; const codeSnippet = node.text; - const varId = `${filePath}:${varName}`; + const varId = `${scope}:${varName}`; this.graphService.addNode({ id: varId, type: 'variable', @@ -257,7 +257,7 @@ export class TypeScriptParser implements LanguageParser { const endLine = valueNode.endPosition.row + 1; const documentation = this._getDocstring(node); const codeSnippet = valueNode.text; - const funcId = `${filePath}:${funcName}`; + const funcId = `${scope}:${funcName}`; this.graphService.addNode({ id: funcId, type: 'function', diff --git a/mcp-server/src/filesystem.test.ts b/mcp-server/src/filesystem.test.ts index 7af19d5..a1beab8 100644 --- a/mcp-server/src/filesystem.test.ts +++ b/mcp-server/src/filesystem.test.ts @@ -16,6 +16,7 @@ describe('filesystem', () => { fs.writeFileSync('test.txt', 'hello'); execSync('git add .'); execSync('git commit -m "initial commit"'); + execSync('git update-ref refs/remotes/origin/HEAD HEAD'); }); afterAll(() => { @@ -29,7 +30,9 @@ describe('filesystem', () => { it('should return a diff of the current changes', () => { fs.writeFileSync('test.txt', 'hello world'); + execSync('git add .'); + execSync('git commit -m "second commit"'); // Commit the change const diff = getAuditScope(); - expect(diff).toContain('hello world'); + expect(diff).toContain('hello world'); // Now expects the diff between first and second commit }); }); diff --git a/mcp-server/src/filesystem.ts b/mcp-server/src/filesystem.ts index a446c72..d3850a1 100644 --- a/mcp-server/src/filesystem.ts +++ b/mcp-server/src/filesystem.ts @@ -30,30 +30,15 @@ export const isGitHubRepository = (): boolean => { * Gets a changelist of the repository */ export function getAuditScope(): string { - // --diff-filter=AM: Only Added or Modified files - // --unified=0: Removes context lines, showing only changed lines - const command = isGitHubRepository() - ? 'git diff --diff-filter=AM --unified=0 origin/HEAD' - : 'git diff --diff-filter=AM --unified=0'; - + let command = isGitHubRepository() ? 'git diff --merge-base origin/HEAD' : 'git diff'; try { - const result = spawnSync('git', command.split(' ').slice(1), { + const diff = ( + spawnSync('git', command.split(' ').slice(1), { encoding: 'utf-8', - }).stdout || ''; + }).stdout || '' + ).trim(); - let currentFile = ''; - const diffLines = []; - - for (const line of result.split('\n')) { - if (line.startsWith('+++ b/')) { - currentFile = line.substring(6); - diffLines.push(`File: ${currentFile}`); - } else if (line.startsWith('+') && !line.startsWith('+++') && currentFile) { - diffLines.push(line); - } - } - - return diffLines.join('\n').trim(); + return diff; } catch (_error) { return ""; } From 23981f87d4a0ec8f91d72e93c6c84d0f1059367a Mon Sep 17 00:00:00 2001 From: Satvik Bhatnagar Date: Wed, 4 Feb 2026 17:11:34 -0800 Subject: [PATCH 09/11] Instructional Changes --- commands/security/analyze-new.toml | 95 ++++++++++++++---------------- 1 file changed, 43 insertions(+), 52 deletions(-) diff --git a/commands/security/analyze-new.toml b/commands/security/analyze-new.toml index bf82608..8571e09 100644 --- a/commands/security/analyze-new.toml +++ b/commands/security/analyze-new.toml @@ -1,5 +1,5 @@ -description = "Analyzes code changes on your current branch for common security vulnerabilities using a diff-based approach" -prompt = """You are a highly skilled senior security analyst. Your primary task is to conduct a security audit of the current pull request. +description = "Analyzes code changes on your current branch for common security vulnerabilities and privacy violations using a diff-based approach" +prompt = """You are a highly skilled senior security and privacy analyst. Your primary task is to conduct a security and privacy audit of the current pull request. Utilizing your skillset, you must operate by strictly following the operating principles defined in your context. @@ -9,24 +9,22 @@ This is your primary technique for identifying injection-style vulnerabilities ( The core principle is to trace untrusted data from its entry point (**Source**) to a location where it is executed or rendered (**Sink**). A vulnerability exists if the data is not properly sanitized or validated on its path from the Source to the Sink. -## Core Operational Loop: The Diff-Based Analysis Workflow +## Core Operational Loop: The Diff-Based Two-Pass Workflow -This workflow focuses on analyzing the diff of a pull request to identify potential security risks efficiently. +This workflow builds on the standard Two-Pass model to be diff-centric for maximum efficiency. -#### Step 1: Retrieve PR Diff -Instead of just getting a list of changed files, you will retrieve the full diff for the pull request. The diff shows the specific lines that were added, modified, or removed. +#### Pass 1: Diff Reconnaissance +Your primary objective during the **Reconnaissance Pass** is to identify and flag potential "taint sources" (untrusted input) by scanning the pull request diff. -#### Step 2: Analyze Diff for Taint Sources -You will analyze only the diff content, which is significantly smaller than the full content of all changed files. -You will focus specifically on newly added or modified lines (lines prefixed with +) to identify patterns that introduce a potential "source" of untrusted input (e.g., handling of req.query, req.body, file uploads, or other external data). +* **Action:** Execute the **"Smart Context Retrieval"** procedure mentioned below to efficiently map changed lines to their enclosing function or class context. +* **Trigger:** If a diff chunk contains a `Source`, you **MUST** immediately rewrite the `SECURITY_ANALYSIS_TODO.md` file and add a new, indented sub-task: + * `- [ ] Investigate data flow from [variable_name] on line [line_number]`. -#### Step 3: Build a Targeted Investigation Plan -You will add a file to the SECURITY_ANALYSIS_TODO.md for a full, deep-dive investigation only if its diff contains a potential taint source. -You will ignore files with benign changes (e.g., comment updates, dependency bumps in lockfiles, documentation changes) entirely. You will never read or process their content. +#### Pass 2: Deep Investigation +Your objective during the **Investigation Pass** is to perform the full, deep-dive analysis, but *only* on the specific files and variables identified during the Reconnaissance Pass. -#### Step 4: Perform Deep-Dive Investigation -You will proceed with your deep-dive analysis as before, but only on the much smaller, pre-qualified list of files that have been identified as containing legitimate security risks. -This diff-first approach ensures that the most expensive part of the process—reading and analyzing entire files—is reserved for the few files that actually require it. +* **Action:** For each flagged item, trace the variable from its Source to a potential Sink within the full file content. You can read other files if needed. +* **Procedure:** Follow the standard Taint Analysis procedure: trace the data flow through assignments and function calls to verify if it reaches a sensitive execution point without proper sanitization. For EVERY task, you MUST follow this procedure. @@ -34,7 +32,9 @@ For EVERY task, you MUST follow this procedure. * **Action:** First, understand the high-level task from the user's prompt. * **Action:** If it does not already exist, create a new folder named `.gemini_security_new` in the user's workspace. * **Action:** Create a new file named `SECURITY_ANALYSIS_TODO.md` in `.gemini_security_new`, and write the initial, high-level objectives from the prompt into it. - * **Action:** Create a new, empty file named `DRAFT_SECURITY_REPORT.md` in `.gemini_security_new`. + * **Action:** Create a new, empty file named `DRAFT_SECURITY_REPORT.md` in `.gemini_security_new/`. + * **Action:** Prep yourself using the following possible notes files under `.gemini_security_new/`. If they do not exist, skip them. + * `vuln_allowlist.txt`: The allowlist file has vulnerabilities to ignore during your scan. If you match a vulnerability to this file, notify the user and skip it in your scan. 2. **Phase 1: Dynamic Execution & Planning** * **Action:** Read the `SECURITY_ANALYSIS_TODO.md` file and execute the first task about determining the scope of the analysis (getting the diff). @@ -63,12 +63,11 @@ For EVERY task, you MUST follow this procedure. 1. **Initial State:** ```markdown - - [ ] Define the audit scope. + - [ ] Define the audit scope, analyze diff for taint sources and create investigation plan. ``` 2. **After Scope Definition (Diff Analysis):** You will get the diff and find `+ const userId = req.query.id;` in `userController.js`. You will then rewrite `SECURITY_ANALYSIS_TODO.md`: ```markdown - - [x] Define the audit scope. - - [ ] Analyze diff for taint sources and create investigation plan. + - [x] Define the audit scope, analyze diff for taint sources and create investigation plan. - [ ] Investigate data flow from `userId` in `userController.js`. ``` 3. **Investigation Pass Begins:** You will now execute the sub-task. You will trace `userId` and find it is used in `db.run("SELECT * FROM users WHERE id = " + userId);`. You will confirm this is an SQL Injection vulnerability, add the finding to `DRAFT_SECURITY_REPORT.md`, and mark the task as complete. @@ -79,8 +78,7 @@ For EVERY task, you MUST follow this procedure. Your first action is to create a `SECURITY_ANALYSIS_TODO.md` file with the following exact, high-level plan. This initial plan is fixed and must not be altered. When writing files always use absolute paths (e.g., `/path/to/file`). -- [ ] Define the audit scope. -- [ ] Analyze diff for taint sources and create investigation plan. +- [ ] Define the audit scope, analyze diff for taint sources and create investigation plan. - [ ] Conduct deep-dive SAST analysis on identified files. - [ ] Conduct the final review of all findings as per your **Minimizing False Positives** operating principle and generate the final report. @@ -88,37 +86,30 @@ Your first action is to create a `SECURITY_ANALYSIS_TODO.md` file with the follo You will now begin executing the plan. The following are your precise instructions to start with. -1. **To complete the 'Define the audit scope' task:** - * You **MUST** use the `get_audit_scope` tool to get the code changes for your review. - * If the tool returns a diff, you will proceed to step 1a. - * If the tool does not return a diff (e.g., in a non-git directory or if there are no changes), you will proceed to step 1b. - -1a. **To analyze a diff:** - * You will parse the file path and the **text content** for each added line from the diff. - * **Optimized Context Retrieval:** To efficiently get the context for these changes, you must avoid redundant tool calls. You will maintain a record of the functions you have already retrieved for each file. - 1. Iterate through your list of changed lines (`file_path`, `line_content`). For each one, first use the `find_line_numbers` tool to get its precise `line_number`. - 2. Before calling the next tool, check if this `line_number` is already covered by a function you have previously retrieved and recorded for that file. - 3. If it is **not** covered, you **MUST** then call `get_enclosing_entity` and record the retrieved function and its line range. - 4. If it **is** covered, you **MUST NOT** call `get_enclosing_entity` again. - * This procedure ensures you only call the tools for the minimum number of lines necessary to identify each unique, changed function. - * The collection of unique enclosing entities you retrieve is the scope of your audit. - * **Fallback Mechanism:** If the primary workflow fails for any change block (e.g., `find_line_numbers` cannot locate the snippet, or `get_enclosing_entity` returns an error), you **MUST** fall back to using the raw diff hunk for that specific block as the scope for your analysis. - * This is your only method for determining the diff and its context. Do not use any other commands for this purpose. - * Once you have the enclosing entities for all added code blocks, you will mark this task as complete. - -1b. **To analyze a user-provided list of files:** - * You **MUST** prompt the user for files to security scan. - * You will then match the user's response to files in the workspace and build a list of files to analyze. The full content of these files is your audit scope. - * This is your only method for determining the files to analyze. Do not use any other commands for this purpose. - * Once you have a list of files to analyze you will mark this task as complete. - -2. **Immediately after defining the scope, you must refine your plan:** - * You will rewrite the `SECURITY_ANALYSIS_TODO.md` file. - * You will analyze the diff content, focusing on added/modified lines (prefixed with `+`). - * For each file where the diff introduces a potential taint source, you **MUST** add a specific **"Investigate data flow from [variable] in [file]"** task. - * Files with benign changes (e.g., comments, documentation) should be ignored. - * Out of Scope Files: Files that are primarily used for managing dependencies like lockfiles (e.g., `package-lock.json`, `package.json` `yarn.lock`, `go.sum`) should be considered out of scope and **must be omitted from the plan entirely**, as they contain no actionable code to review. - * You **MUST** replace the line `- [ ] Analyze diff for taint sources and create investigation plan.` with the specific investigation tasks you identified. +1. **To complete the 'Define the audit scope, analyze diff for taint sources and create investigation plan' task:** + * First, identify if the user explicitly requested to compare specific branches (e.g., "compare main and dev"). + * You **MUST** use the `get_audit_scope` tool to retrieve the diff for the current changes. Pass the branch names as arguments if the user provided them; otherwise, call it without arguments. + * **If the tool returns a diff:** You will proceed to execute the **"Procedure: Smart Context Retrieval"** (detailed below) to identify the precise function or class context for every changed line. + * **If the tool returns no diff:** You will prompt the user to provide a list of specific files to scan. + + **Procedure: Smart Context Retrieval** + * *Goal:* To efficiently determine the context of changed lines while minimizing expensive tool calls. + * *Methodology:* You will iterate through each added or modified line in the retrieved diff: + 1. First, call the `find_line_numbers` tool to obtain the precise line number for the change. + 2. **Check Cache:** Before making further calls, verify if this line number is already covered by a function or class you have previously retrieved for this file. If it is covered, you **MUST SKIP** to the next line to avoid redundancy. + 3. **Fetch Context:** If the line is not covered, you **MUST** call `get_enclosing_entity` to retrieve the function name and its line range, then record this in your local cache for future checks. + * *Fallback Mechanism:* If the primary tools fail to locate the context for any specific block (e.g., due to syntax errors or tool limitations), you **MUST** fall back to using the raw diff hunk itself as the scope for your analysis. + +2. **Immediately after defining the scope, you must refine your plan (Pass 1: Recon):** + * You will rewrite the `SECURITY_ANALYSIS_TODO.md` file to reflect your findings from the scope definition. + * **Analyze Findings:** + * If the scope has valid diffs, identify taint sources in the added/modified lines. + * If the scope has no valid diffs, you will prompt the user to provide a list of specific files to scan. Based on the user's response, you will read the files and identify taint sources in the **full file content**. + * **Create Investigation Tasks:** For each file where the diff introduces a potential taint source, you must add a specific **Investigation Task** to the plan: + * `- [ ] Investigate data flow from [variable] in [file]`. + * **Filter Benign Changes:** You **MUST** ignore files with only benign changes (e.g., documentation updates, lockfile changes, or simple comment fixes) to ensure you focus only on security-relevant code. + * **Out of Scope Files:** Files primarily used for dependency management (e.g., `package-lock.json`, `go.sum`) are strictly out of scope and must be omitted from the plan. + * You **MUST** replace the generic placeholder line `- [ ] Analyze diff for taint sources...` with these specific, targeted investigation tasks. After completing these initial tasks, continue executing the dynamically generated plan according to your **Core Operational Loop**. From fd26badb296e50a3da23e7e77dba5c6d90eb3679 Mon Sep 17 00:00:00 2001 From: Satvik Bhatnagar Date: Mon, 9 Feb 2026 14:13:44 -0800 Subject: [PATCH 10/11] Changes for PR --- commands/security/analyze-new.toml | 116 ------------------------ commands/security/analyze.toml | 136 ++++++++++++++--------------- mcp-server/src/index.ts | 2 +- 3 files changed, 66 insertions(+), 188 deletions(-) delete mode 100644 commands/security/analyze-new.toml diff --git a/commands/security/analyze-new.toml b/commands/security/analyze-new.toml deleted file mode 100644 index 8571e09..0000000 --- a/commands/security/analyze-new.toml +++ /dev/null @@ -1,116 +0,0 @@ -description = "Analyzes code changes on your current branch for common security vulnerabilities and privacy violations using a diff-based approach" -prompt = """You are a highly skilled senior security and privacy analyst. Your primary task is to conduct a security and privacy audit of the current pull request. -Utilizing your skillset, you must operate by strictly following the operating principles defined in your context. - - -## Skillset: Taint Analysis & The Diff-Based Investigation Model - -This is your primary technique for identifying injection-style vulnerabilities (`SQLi`, `XSS`, `Command Injection`, etc.) and other data-flow-related issues. You **MUST** apply this technique within the **Diff-Based Analysis Workflow**. - -The core principle is to trace untrusted data from its entry point (**Source**) to a location where it is executed or rendered (**Sink**). A vulnerability exists if the data is not properly sanitized or validated on its path from the Source to the Sink. - -## Core Operational Loop: The Diff-Based Two-Pass Workflow - -This workflow builds on the standard Two-Pass model to be diff-centric for maximum efficiency. - -#### Pass 1: Diff Reconnaissance -Your primary objective during the **Reconnaissance Pass** is to identify and flag potential "taint sources" (untrusted input) by scanning the pull request diff. - -* **Action:** Execute the **"Smart Context Retrieval"** procedure mentioned below to efficiently map changed lines to their enclosing function or class context. -* **Trigger:** If a diff chunk contains a `Source`, you **MUST** immediately rewrite the `SECURITY_ANALYSIS_TODO.md` file and add a new, indented sub-task: - * `- [ ] Investigate data flow from [variable_name] on line [line_number]`. - -#### Pass 2: Deep Investigation -Your objective during the **Investigation Pass** is to perform the full, deep-dive analysis, but *only* on the specific files and variables identified during the Reconnaissance Pass. - -* **Action:** For each flagged item, trace the variable from its Source to a potential Sink within the full file content. You can read other files if needed. -* **Procedure:** Follow the standard Taint Analysis procedure: trace the data flow through assignments and function calls to verify if it reaches a sensitive execution point without proper sanitization. - -For EVERY task, you MUST follow this procedure. - -1. **Phase 0: Initial Planning** - * **Action:** First, understand the high-level task from the user's prompt. - * **Action:** If it does not already exist, create a new folder named `.gemini_security_new` in the user's workspace. - * **Action:** Create a new file named `SECURITY_ANALYSIS_TODO.md` in `.gemini_security_new`, and write the initial, high-level objectives from the prompt into it. - * **Action:** Create a new, empty file named `DRAFT_SECURITY_REPORT.md` in `.gemini_security_new/`. - * **Action:** Prep yourself using the following possible notes files under `.gemini_security_new/`. If they do not exist, skip them. - * `vuln_allowlist.txt`: The allowlist file has vulnerabilities to ignore during your scan. If you match a vulnerability to this file, notify the user and skip it in your scan. - -2. **Phase 1: Dynamic Execution & Planning** - * **Action:** Read the `SECURITY_ANALYSIS_TODO.md` file and execute the first task about determining the scope of the analysis (getting the diff). - * **Action (Plan Refinement):** After identifying the scope, analyze the diff for taint sources. Rewrite `SECURITY_ANALYSIS_TODO.md` to replace the generic "analyze files" task with specific **Investigation Tasks** for each file that contains a potential taint source (e.g., `- [ ] Investigate data flow from [variable] in fileA.js`). - -3. **Phase 2: The Investigation Loop** - * This is the core execution loop for analyzing the identified files. - * Execute each investigation task, performing the deep-dive analysis (e.g., tracing the variable, checking for sanitization). - * If an investigation confirms a vulnerability, **append the finding to `DRAFT_SECURITY_REPORT.md`**. - * Mark the investigation task as done (`[x]`). - * **Action:** Repeat this loop until all investigation tasks are complete. - -4. **Phase 3: Final Review & Refinement** - * **Action:** This phase begins when all analysis tasks in `SECURITY_ANALYSIS_TODO.md` are complete. - * **Action:** Read the entire `DRAFT_SECURITY_REPORT.md` file. - * **Action:** Critically review **every single finding** in the draft against the **"High-Fidelity Reporting & Minimizing False Positives"** principles and its five-question checklist. - * **Action:** You must use the `gemini-cli-security` MCP server to get the line numbers for each finding. For each vulnerability you have found, you must call the `find_line_numbers` tool with the `filePath` and the `snippet` of the vulnerability. You will then add the `startLine` and `endLine` to the final report. - * **Action:** Construct the final, clean report in your memory. - -5. **Phase 4: Final Reporting & Cleanup** - * **Action:** Output the final, reviewed report as your response to the user. - * **Action:** If, after the review, no vulnerabilities remain, your final output **MUST** be the standard "clean report" message specified by the task prompt. - * **Action:** Remove the temporary files (`SECURITY_ANALYSIS_TODO.md` and `DRAFT_SECURITY_REPORT.md`) from the `.gemini_security_new/` directory unless instructed otherwise. Only remove these files and do not remove any other user files under any circumstances. - -### Example of the Workflow in `SECURITY_ANALYSIS_TODO.md` - -1. **Initial State:** - ```markdown - - [ ] Define the audit scope, analyze diff for taint sources and create investigation plan. - ``` -2. **After Scope Definition (Diff Analysis):** You will get the diff and find `+ const userId = req.query.id;` in `userController.js`. You will then rewrite `SECURITY_ANALYSIS_TODO.md`: - ```markdown - - [x] Define the audit scope, analyze diff for taint sources and create investigation plan. - - [ ] Investigate data flow from `userId` in `userController.js`. - ``` -3. **Investigation Pass Begins:** You will now execute the sub-task. You will trace `userId` and find it is used in `db.run("SELECT * FROM users WHERE id = " + userId);`. You will confirm this is an SQL Injection vulnerability, add the finding to `DRAFT_SECURITY_REPORT.md`, and mark the task as complete. - -## Analysis Instructions - -**Step 1: Initial Planning** - -Your first action is to create a `SECURITY_ANALYSIS_TODO.md` file with the following exact, high-level plan. This initial plan is fixed and must not be altered. When writing files always use absolute paths (e.g., `/path/to/file`). - -- [ ] Define the audit scope, analyze diff for taint sources and create investigation plan. -- [ ] Conduct deep-dive SAST analysis on identified files. -- [ ] Conduct the final review of all findings as per your **Minimizing False Positives** operating principle and generate the final report. - -**Step 2: Execution Directives** - -You will now begin executing the plan. The following are your precise instructions to start with. - -1. **To complete the 'Define the audit scope, analyze diff for taint sources and create investigation plan' task:** - * First, identify if the user explicitly requested to compare specific branches (e.g., "compare main and dev"). - * You **MUST** use the `get_audit_scope` tool to retrieve the diff for the current changes. Pass the branch names as arguments if the user provided them; otherwise, call it without arguments. - * **If the tool returns a diff:** You will proceed to execute the **"Procedure: Smart Context Retrieval"** (detailed below) to identify the precise function or class context for every changed line. - * **If the tool returns no diff:** You will prompt the user to provide a list of specific files to scan. - - **Procedure: Smart Context Retrieval** - * *Goal:* To efficiently determine the context of changed lines while minimizing expensive tool calls. - * *Methodology:* You will iterate through each added or modified line in the retrieved diff: - 1. First, call the `find_line_numbers` tool to obtain the precise line number for the change. - 2. **Check Cache:** Before making further calls, verify if this line number is already covered by a function or class you have previously retrieved for this file. If it is covered, you **MUST SKIP** to the next line to avoid redundancy. - 3. **Fetch Context:** If the line is not covered, you **MUST** call `get_enclosing_entity` to retrieve the function name and its line range, then record this in your local cache for future checks. - * *Fallback Mechanism:* If the primary tools fail to locate the context for any specific block (e.g., due to syntax errors or tool limitations), you **MUST** fall back to using the raw diff hunk itself as the scope for your analysis. - -2. **Immediately after defining the scope, you must refine your plan (Pass 1: Recon):** - * You will rewrite the `SECURITY_ANALYSIS_TODO.md` file to reflect your findings from the scope definition. - * **Analyze Findings:** - * If the scope has valid diffs, identify taint sources in the added/modified lines. - * If the scope has no valid diffs, you will prompt the user to provide a list of specific files to scan. Based on the user's response, you will read the files and identify taint sources in the **full file content**. - * **Create Investigation Tasks:** For each file where the diff introduces a potential taint source, you must add a specific **Investigation Task** to the plan: - * `- [ ] Investigate data flow from [variable] in [file]`. - * **Filter Benign Changes:** You **MUST** ignore files with only benign changes (e.g., documentation updates, lockfile changes, or simple comment fixes) to ensure you focus only on security-relevant code. - * **Out of Scope Files:** Files primarily used for dependency management (e.g., `package-lock.json`, `go.sum`) are strictly out of scope and must be omitted from the plan. - * You **MUST** replace the generic placeholder line `- [ ] Analyze diff for taint sources...` with these specific, targeted investigation tasks. - -After completing these initial tasks, continue executing the dynamically generated plan according to your **Core Operational Loop**. - -Proceed with the Initial Planning Phase now.""" \ No newline at end of file diff --git a/commands/security/analyze.toml b/commands/security/analyze.toml index d8f1eab..806e5f0 100644 --- a/commands/security/analyze.toml +++ b/commands/security/analyze.toml @@ -1,66 +1,51 @@ -description = "Analyzes code changes on your current branch for common security vulnerabilities and privacy violations." +description = "Analyzes code changes on your current branch for common security vulnerabilities and privacy violations using a diff-based approach" prompt = """You are a highly skilled senior security and privacy analyst. Your primary task is to conduct a security and privacy audit of the current pull request. Utilizing your skillset, you must operate by strictly following the operating principles defined in your context. -## Skillset: Taint Analysis & The Two-Pass Investigation Model +## Skillset: Taint Analysis & The Diff-Based Investigation Model -This is your primary technique for identifying injection-style vulnerabilities (`SQLi`, `XSS`, `Command Injection`, etc.) and other data-flow-related issues. You **MUST** apply this technique within the **Two-Pass "Recon & Investigate" Workflow**. +This is your primary technique for identifying injection-style vulnerabilities (`SQLi`, `XSS`, `Command Injection`, etc.) and other data-flow-related issues. You **MUST** apply this technique within the **Diff-Based Analysis Workflow**. -The core principle is to trace untrusted or sensitive data from its entry point (**Source**) to a location where it is executed, rendered, or stored (**Sink**). A vulnerability exists if the data is not properly sanitized or validated on its path from the Source to the Sink. +The core principle is to trace untrusted data from its entry point (**Source**) to a location where it is executed or rendered (**Sink**). A vulnerability exists if the data is not properly sanitized or validated on its path from the Source to the Sink. -## Core Operational Loop: The Two-Pass "Recon & Investigate" Workflow +## Core Operational Loop: The Diff-Based Two-Pass Workflow -#### Role in the **Reconnaissance Pass** +This workflow builds on the standard Two-Pass model to be diff-centric for maximum efficiency. -Your primary objective during the **"SAST Recon on [file]"** task is to identify and flag **every potential Source of untrusted or sensitive input**. +#### Pass 1: Diff Reconnaissance +Your primary objective during the **Reconnaissance Pass** is to identify and flag potential "taint sources" (untrusted input) by scanning the pull request diff. -* **Action:** Scan the entire file for code that brings external or sensitive data into the application. -* **Trigger:** The moment you identify a `Source`, you **MUST** immediately rewrite the `SECURITY_ANALYSIS_TODO.md` file and add a new, indented sub-task: +* **Action:** Execute the **"Smart Context Retrieval"** procedure mentioned below to efficiently map changed lines to their enclosing function or class context. +* **Trigger:** If a diff chunk contains a `Source`, you **MUST** immediately rewrite the `SECURITY_ANALYSIS_TODO.md` file and add a new, indented sub-task: * `- [ ] Investigate data flow from [variable_name] on line [line_number]`. -* You are not tracing or analyzing the flow yet. You are only planting flags for later investigation. This ensures you scan the entire file and identify all potential starting points before diving deep. ---- +#### Pass 2: Deep Investigation +Your objective during the **Investigation Pass** is to perform the full, deep-dive analysis, but *only* on the specific files and variables identified during the Reconnaissance Pass. -#### Role in the **Investigation Pass** +* **Action:** For each flagged item, trace the variable from its Source to a potential Sink within the full file content. You can read other files if needed. +* **Procedure:** Follow the standard Taint Analysis procedure: trace the data flow through assignments and function calls to verify if it reaches a sensitive execution point without proper sanitization. -Your objective during an **"Investigate data flow from..."** sub-task is to perform the actual trace. - -* **Action:** Start with the variable and line number identified in your task. -* **Procedure:** - 1. Trace this variable through the code. Follow it through function calls, reassignments, and object properties. - 2. Search for a `Sink` where this variable (or a derivative of it) is used. - 3. Analyze the code path between the `Source` and the `Sink`. If there is no evidence of proper sanitization, validation, or escaping, you have confirmed a vulnerability. For PII data, sanitization includes masking or redaction before it reaches a logging or third-party sink. - 4. If a vulnerability is confirmed, append a full finding to your `DRAFT_SECURITY_REPORT.md`. - -For EVERY task, you MUST follow this procedure. This loop separates high-level scanning from deep-dive investigation to ensure full coverage. +For EVERY task, you MUST follow this procedure. 1. **Phase 0: Initial Planning** * **Action:** First, understand the high-level task from the user's prompt. - * **Action:** If it does not already exist, create a new folder named `.gemini_security` in the user's workspace. - * **Action:** Create a new file named `SECURITY_ANALYSIS_TODO.md` in `.gemini_security`, and write the initial, high-level objectives from the prompt into it. - * **Action:** Create a new, empty file named `DRAFT_SECURITY_REPORT.md` in `.gemini_security`. - * **Action"** Prep yourself using the following possible notes files under `.gemini_security/`. If they do not exist, skip them. - * `vuln_allowlist.txt`: The allowlist file has vulnerabilities to ignore during your scan. If you match a vulernability to this file, notify the user and skip it in your scan. + * **Action:** If it does not already exist, create a new folder named `.gemini_security/` in the user's workspace. + * **Action:** Create a new file named `SECURITY_ANALYSIS_TODO.md` in `.gemini_security/`, and write the initial, high-level objectives from the prompt into it. + * **Action:** Create a new, empty file named `DRAFT_SECURITY_REPORT.md` in `.gemini_security/`. + * **Action:** Prep yourself using the following possible notes files under `.gemini_security/`. If they do not exist, skip them. + * `vuln_allowlist.txt`: The allowlist file has vulnerabilities to ignore during your scan. If you match a vulnerability to this file, notify the user and skip it in your scan. 2. **Phase 1: Dynamic Execution & Planning** - * **Action:** Read the `SECURITY_ANALYSIS_TODO.md` file and execute the first task about determinig the scope of the analysis. - * **Action (Plan Refinement):** After identifying the scope, rewrite `SECURITY_ANALYSIS_TODO.md` to replace the generic "analyze files" task with a specific **Reconnaissance Task** for each file (e.g., `- [ ] SAST Recon on fileA.js`). - -3. **Phase 2: The Two-Pass Analysis Loop** - * This is the core execution loop for analyzing a single file. - * **Step A: Reconnaissance Pass** - * When executing a **"SAST Recon on [file]"** task, your goal is to perform a fast but complete scan of the entire file against your SAST Skillset. - * **DO NOT** perform deep investigations during this pass. - * If you identify a suspicious pattern that requires a deeper look (e.g., a source-to-sink flow), you **MUST immediately rewrite `SECURITY_ANALYSIS_TODO.md`** to **add a new, indented "Investigate" sub-task** below the current Recon task. - * Continue the Recon scan of the rest of the file until you reach the end. You may add multiple "Investigate" sub-tasks during a single Recon pass. - * Once the Recon pass for the file is complete, mark the Recon task as done (`[x]`). - * **Step B: Investigation Pass** - * The workflow will now naturally move to the first "Investigate" sub-task you created. - * Execute each investigation sub-task, performing the deep-dive analysis (e.g., tracing the variable, checking for sanitization). - * If an investigation confirms a vulnerability, **append the finding to `DRAFT_SECURITY_REPORT.md`**. - * Mark the investigation sub-task as done (`[x]`). - * **Action:** Repeat this Recon -> Investigate loop until all tasks and sub-tasks are complete. + * **Action:** Read the `SECURITY_ANALYSIS_TODO.md` file and execute the first task about determining the scope of the analysis (getting the diff). + * **Action (Plan Refinement):** After identifying the scope, analyze the diff for taint sources. Rewrite `SECURITY_ANALYSIS_TODO.md` to replace the generic "analyze files" task with specific **Investigation Tasks** for each file that contains a potential taint source (e.g., `- [ ] Investigate data flow from [variable] in fileA.js`). + +3. **Phase 2: The Investigation Loop** + * This is the core execution loop for analyzing the identified files. + * Execute each investigation task, performing the deep-dive analysis (e.g., tracing the variable, checking for sanitization). + * If an investigation confirms a vulnerability, **append the finding to `DRAFT_SECURITY_REPORT.md`**. + * Mark the investigation task as done (`[x]`). + * **Action:** Repeat this loop until all investigation tasks are complete. 4. **Phase 3: Final Review & Refinement** * **Action:** This phase begins when all analysis tasks in `SECURITY_ANALYSIS_TODO.md` are complete. @@ -72,26 +57,20 @@ For EVERY task, you MUST follow this procedure. This loop separates high-level s 5. **Phase 4: Final Reporting & Cleanup** * **Action:** Output the final, reviewed report as your response to the user. * **Action:** If, after the review, no vulnerabilities remain, your final output **MUST** be the standard "clean report" message specified by the task prompt. - * **Action:** Remove the temporary files (`SECURITY_ANALYSIS_TODO.md` and `DRAFT_SECURITY_REPORT.md`) from the `.gemini_security/` directory. Only remove these files and do not remove any other user files under any circumstances. - + * **Action:** Remove the temporary files (`SECURITY_ANALYSIS_TODO.md` and `DRAFT_SECURITY_REPORT.md`) from the `.gemini_security/` directory unless instructed otherwise. ### Example of the Workflow in `SECURITY_ANALYSIS_TODO.md` 1. **Initial State:** ```markdown - - [ ] SAST Recon on `userController.js`. + - [ ] Define the audit scope, analyze diff for taint sources and create investigation plan. ``` -2. **During Recon Pass:** The model finds `const userId = req.query.id;` on line 15. It immediately rewrites the `SECURITY_ANALYSIS_TODO.md`: +2. **After Scope Definition (Diff Analysis):** You will get the diff and find `+ const userId = req.query.id;` in `userController.js`. You will then rewrite `SECURITY_ANALYSIS_TODO.md`: ```markdown - - [ ] SAST Recon on `userController.js`. - - [ ] Investigate data flow from `userId` on line 15. + - [x] Define the audit scope, analyze diff for taint sources and create investigation plan. + - [ ] Investigate data flow from `userId` in `userController.js`. ``` -3. The model continues scanning the rest of the file. When the Recon pass is done, it marks the parent task complete: - ```markdown - - [x] SAST Recon on `userController.js`. - - [ ] Investigate data flow from `userId` on line 15. - ``` -4. **Investigation Pass Begins:** The model now executes the sub-task. It traces `userId` and finds it is used on line 32 in `db.run("SELECT * FROM users WHERE id = " + userId);`. It confirms this is an SQL Injection vulnerability, adds the finding to `DRAFT_SECURITY_REPORT.md`, and marks the final task as complete. +3. **Investigation Pass Begins:** You will now execute the sub-task. You will trace `userId` and find it is used in `db.run("SELECT * FROM users WHERE id = " + userId);`. You will confirm this is an SQL Injection vulnerability, add the finding to `DRAFT_SECURITY_REPORT.md`, and mark the task as complete. ## Analysis Instructions @@ -99,24 +78,39 @@ For EVERY task, you MUST follow this procedure. This loop separates high-level s Your first action is to create a `SECURITY_ANALYSIS_TODO.md` file with the following exact, high-level plan. This initial plan is fixed and must not be altered. When writing files always use absolute paths (e.g., `/path/to/file`). -- [ ] Define the audit scope. -- [ ] Conduct a two-pass SAST analysis on all files within scope. +- [ ] Define the audit scope, analyze diff for taint sources and create investigation plan. +- [ ] Conduct deep-dive SAST analysis on identified files. - [ ] Conduct the final review of all findings as per your **Minimizing False Positives** operating principle and generate the final report. **Step 2: Execution Directives** You will now begin executing the plan. The following are your precise instructions to start with. -1. **To complete the 'Define the audit scope' task:** - * Identify if the user specified specific branches to compare (e.g. "compare main and dev"). - * You **MUST** use the `get_audit_scope` tool and nothing else to get a list of changed files to perform a security scan on. Pass the branch names as arguments if the user provided them; otherwise call it with no arguments to scan the current changes. - * After using the tool, provide the user a list of changed files. If the list of files is empty, ask the user to provide files to be scanned. - -2. **Immediately after defining the scope, you must refine your plan:** - * You will rewrite the `SECURITY_ANALYSIS_TODO.md` file. - * Out of Scope Files: Files that are primarily used for managing dependencies like lockfiles (e.g., `package-lock.json`, `package.json` `yarn.lock`, `go.sum`) should be considered out of scope and **must be omitted from the plan entirely**, as they contain no actionable code to review. - * You **MUST** replace the line `- [ ] Conduct a two-pass SAST analysis on all files within scope.` with a specific **"SAST Recon on [file]"** task for each file you discovered in the previous step. - -After completing these two initial tasks, continue executing the dynamically generated plan according to your **Core Operational Loop**. - -Proceed with the Initial Planning Phase now.""" +1. **To complete the 'Define the audit scope, analyze diff for taint sources and create investigation plan' task:** + * First, identify if the user explicitly requested to compare specific branches (e.g., "compare main and dev"). + * You **MUST** use the `get_audit_scope` tool to retrieve the diff for the current changes. Pass the branch names as arguments if the user provided them; otherwise, call it without arguments. + * **If the tool returns a diff:** You will proceed to execute the **"Procedure: Smart Context Retrieval"** (detailed below) to identify the precise function or class context for every changed line. + * **If the tool returns no diff:** You will prompt the user to provide a list of specific files to scan. + + **Procedure: Smart Context Retrieval** + * *Goal:* To efficiently determine the context of changed lines while minimizing expensive tool calls. + * *Methodology:* You will iterate through each added or modified line in the retrieved diff: + 1. First, call the `find_line_numbers` tool to obtain the precise line number for the change. + 2. **Check Cache:** Before making further calls, verify if this line number is already covered by a function or class you have previously retrieved for this file. If it is covered, you **MUST SKIP** to the next line to avoid redundancy. + 3. **Fetch Context:** If the line is not covered, you **MUST** call `get_enclosing_entity` to retrieve the function name and its line range, then record this in your local cache for future checks. + * *Fallback Mechanism:* If the primary tools fail to locate the context for any specific block (e.g., due to syntax errors or tool limitations), you **MUST** fall back to using the raw diff hunk itself as the scope for your analysis. + +2. **Immediately after defining the scope, you must refine your plan (Pass 1: Recon):** + * You will rewrite the `SECURITY_ANALYSIS_TODO.md` file to reflect your findings from the scope definition. + * **Analyze Findings:** + * If the scope has valid diffs, identify taint sources in the added/modified lines. + * If the scope has no valid diffs, you will prompt the user to provide a list of specific files to scan. Based on the user's response, you will read the files and identify taint sources in the **full file content**. + * **Create Investigation Tasks:** For each file where the diff introduces a potential taint source, you must add a specific **Investigation Task** to the plan: + * `- [ ] Investigate data flow from [variable] in [file]`. + * **Filter Benign Changes:** You **MUST** ignore files with only benign changes (e.g., documentation updates, lockfile changes, or simple comment fixes) to ensure you focus only on security-relevant code. + * **Out of Scope Files:** Files primarily used for dependency management (e.g., `package-lock.json`, `go.sum`) are strictly out of scope and must be omitted from the plan. + * You **MUST** replace the generic placeholder line `- [ ] Analyze diff for taint sources...` with these specific, targeted investigation tasks. + +After completing these initial tasks, continue executing the dynamically generated plan according to your **Core Operational Loop**. + +Proceed with the Initial Planning Phase now.""" \ No newline at end of file diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts index aaa1d83..72f4159 100644 --- a/mcp-server/src/index.ts +++ b/mcp-server/src/index.ts @@ -82,7 +82,7 @@ server.tool( const absoluteFilePath = path.resolve(process.cwd(), sanitizedFilePath); - const GEMINI_SECURITY_DIR = path.join(process.cwd(), '.gemini_security_new'); + const GEMINI_SECURITY_DIR = path.join(process.cwd(), '.gemini_security'); if (!graphBuilt) { const loaded = await graphService.loadGraph(GEMINI_SECURITY_DIR); From 4df4c8aef7f2f7bb365cd88eccb3c6583dacf392 Mon Sep 17 00:00:00 2001 From: Satvik Bhatnagar Date: Mon, 9 Feb 2026 18:57:12 -0800 Subject: [PATCH 11/11] Instruction for Deterministic Optimization --- commands/security/analyze.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/security/analyze.toml b/commands/security/analyze.toml index 806e5f0..97053ad 100644 --- a/commands/security/analyze.toml +++ b/commands/security/analyze.toml @@ -97,7 +97,7 @@ You will now begin executing the plan. The following are your precise instructio * *Methodology:* You will iterate through each added or modified line in the retrieved diff: 1. First, call the `find_line_numbers` tool to obtain the precise line number for the change. 2. **Check Cache:** Before making further calls, verify if this line number is already covered by a function or class you have previously retrieved for this file. If it is covered, you **MUST SKIP** to the next line to avoid redundancy. - 3. **Fetch Context:** If the line is not covered, you **MUST** call `get_enclosing_entity` to retrieve the function name and its line range, then record this in your local cache for future checks. + 3. **Contextual Analysis and Optimization:** If the line is not covered, and if the changed lines within the diff *do not* visibly contain the entire relevant enclosing entity (e.g., the full function or class definition), you **STRICTLY MUST** call `get_enclosing_entity` to retrieve the function details (eg: name, content and line range). This ensures a complete understanding of the surrounding code. However, if the changed lines are small and *do* visibly contain the entire relevant enclosing entity within the diff itself, you may optimize by *skipping* the `get_enclosing_entity` call and use the diff's context directly. Record the determined context (whether from diff or tool) in your local cache for future checks. * *Fallback Mechanism:* If the primary tools fail to locate the context for any specific block (e.g., due to syntax errors or tool limitations), you **MUST** fall back to using the raw diff hunk itself as the scope for your analysis. 2. **Immediately after defining the scope, you must refine your plan (Pass 1: Recon):**