From 50b72c0929cd540232cc8cf05d19637dfa9403b2 Mon Sep 17 00:00:00 2001 From: Janet Vu Date: Fri, 6 Feb 2026 04:02:05 +0000 Subject: [PATCH 1/6] Add files / tests for outputting security reports as JSON when requested --- commands/security/analyze.toml | 10 +- mcp-server/src/index.ts | 29 +++++ mcp-server/src/parser.test.ts | 186 +++++++++++++++++++++++++++++++++ mcp-server/src/parser.ts | 115 ++++++++++++++++++++ 4 files changed, 339 insertions(+), 1 deletion(-) create mode 100644 mcp-server/src/parser.test.ts create mode 100644 mcp-server/src/parser.ts diff --git a/commands/security/analyze.toml b/commands/security/analyze.toml index d8f1eab..68f9cf5 100644 --- a/commands/security/analyze.toml +++ b/commands/security/analyze.toml @@ -20,6 +20,13 @@ Your primary objective during the **"SAST Recon on [file]"** task is to identify * `- [ ] 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. +## Skillset: Format Handling +* **Default Behavior**: Always provide the security analysis report in Markdown format within the conversation and as a file. +* **Conditional JSON Output**: If the user explicitly requests JSON (e.g., via a flag like `--json` or natural language), you MUST: + 1. Complete the standard Markdown report first. + 2. Call the `convert_report_to_json` tool using the path to the final report. + 3. Inform the user where the JSON file has been saved. + --- #### Role in the **Investigation Pass** @@ -67,12 +74,13 @@ For EVERY task, you MUST follow this procedure. This loop separates high-level s * **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:** If the user requested JSON output (e.g., via `--json` in context), call the `convert_report_to_json` tool now, using the current `DRAFT_SECURITY_REPORT.md` as the source. * **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. Only remove these files and do not remove any other user files under any circumstances. + * **Action:** ONLY AFTER the final report is delivered and any requested JSON conversion is complete, 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. ### Example of the Workflow in `SECURITY_ANALYSIS_TODO.md` diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts index 2ebd816..8618467 100644 --- a/mcp-server/src/index.ts +++ b/mcp-server/src/index.ts @@ -13,6 +13,7 @@ import { promises as fs } from 'fs'; import path from 'path'; import { getAuditScope } from './filesystem.js'; import { findLineNumbers } from './security.js'; +import { parseMarkdownToDict } from './parser.js'; import { runPoc } from './poc.js'; @@ -64,6 +65,34 @@ server.tool( (input: { filePath: string }) => runPoc(input) ); +server.tool( + 'convert_report_to_json', + 'Converts a Markdown security report into a structured JSON format.', + { + reportPath: z.string().describe('Absolute path to the Markdown report file.'), + outputPath: z.string().optional().describe('Optional path to save the JSON output.'), + } as any, + (async (input: { reportPath: string; outputPath?: string }) => { + try { + const content = await fs.readFile(input.reportPath, 'utf-8'); + const results = parseMarkdownToDict(content); + + if (input.outputPath) { + await fs.writeFile(input.outputPath, JSON.stringify(results, null, 2)); + } + + return { + content: [{ type: 'text', text: JSON.stringify(results) }] + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }], + isError: true + }; + } + }) as any +); + server.registerPrompt( 'security:note-adder', { diff --git a/mcp-server/src/parser.test.ts b/mcp-server/src/parser.test.ts new file mode 100644 index 0000000..e44c794 --- /dev/null +++ b/mcp-server/src/parser.test.ts @@ -0,0 +1,186 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { parseMarkdownToDict } from './parser.js'; + +describe('parseMarkdownToDict', () => { + it('should parse a standard security vulnerability correctly', () => { + const mdContent = ` +Vulnerability: Hardcoded API Key +Vulnerability Type: Security +Severity: Critical +Source Location: config/settings.js:15-15 +Line Content: const KEY = "sk_live_12345"; +Description: A production secret was found hardcoded in the source. +Recommendation: Move the secret to an environment variable. + `; + + const results = parseMarkdownToDict(mdContent); + + expect(results).toHaveLength(1); + expect(results[0]).toMatchObject({ + vulnerability: 'Hardcoded API Key', + vulnerabilityType: 'Security', + severity: 'Critical', + lineContent: 'const KEY = "sk_live_12345";', + extension: { + sourceLocation: { + File: 'config/settings.js', + startLine: 15, + endLine: 15 + } + } + }); + }); + + it('should parse a privacy violation with Sink and Data Type', () => { + const mdContent = ` +Vulnerability: PII Leak in Logs +Vulnerability Type: Privacy +Severity: Medium +Source Location: src/auth.ts:22 +Sink Location: console.log:45 +Data Type: Email Address +Line Content: logger.info("User logged in: " + user.email); +Description: Unmasked email addresses are being written to application logs. +Recommendation: Redact the email address before logging. + `; + + const results = parseMarkdownToDict(mdContent); + + expect(results).toHaveLength(1); + expect(results[0].extension).toMatchObject({ + sinkLocation: { + File: 'console.log', + startLine: 45, + endLine: 45 + }, + dataType: 'Email Address' + }); + }); + + it('should handle multiple vulnerabilities in one file', () => { + const mdContent = ` +Vulnerability: SQL Injection +Vulnerability Type: Security +Severity: High +Source Location: db.js:10 +Line Content: query = "SELECT * FROM users WHERE id = " + id; +Description: Raw input used in query. +Recommendation: Use parameterized queries. + +Vulnerability: Reflected XSS +Vulnerability Type: Security +Severity: Medium +Source Location: app.js:100 +Line Content: res.send("Hello " + req.query.name); +Description: User input rendered without escaping. +Recommendation: Use a templating engine with auto-escaping. + `; + + const results = parseMarkdownToDict(mdContent); + expect(results).toHaveLength(2); + expect(results[0].vulnerability).toBe('SQL Injection'); + expect(results[1].vulnerability).toBe('Reflected XSS'); + }); + + it('should handle markdown formatting like bolding and bullets', () => { + const mdContent = ` +* **Vulnerability:** Hardcoded Secret +- **Severity:** High +* **Source Location:** \`index.js:5-10\` +- **Line Content:** \`\`\`javascript + const secret = "password"; + \`\`\` + `; + + const results = parseMarkdownToDict(mdContent); + + expect(results[0].vulnerability).toBe('Hardcoded Secret'); + expect(results[0].severity).toBe('High'); + expect(results[0].extension.sourceLocation.File).toBe('index.js'); + expect(results[0].lineContent).toBe('const secret = "password";'); + }); + + it('should return empty array if no "Vulnerability:" trigger is found', () => { + const mdContent = "This is a summary report with no specific findings."; + const results = parseMarkdownToDict(mdContent); + expect(results).toHaveLength(0); + }); + + it('should handle missing line numbers and sink location', () => { + const mdContent = ` +Vulnerability: Missing Line Numbers +Vulnerability Type: Security +Severity: High +Source Location: src/index.ts +Line Content: const apiKey = process.env.API_KEY; +Description: Source location without line numbers. +Recommendation: Verify the vulnerability details. + `; + + const results = parseMarkdownToDict(mdContent); + + expect(results).toHaveLength(1); + expect(results[0]).toMatchObject({ + vulnerability: 'Missing Line Numbers', + vulnerabilityType: 'Security', + severity: 'High', + lineContent: 'const apiKey = process.env.API_KEY;' + }); + expect(results[0].extension.sourceLocation.File).toBe('src/index.ts'); + }); + + it('should handle missing end line number', () => { + const mdContent = ` +Vulnerability: No End Line +Vulnerability Type: Security +Severity: Medium +Source Location: app.js:42 +Line Content: res.send(userInput); +Description: Source location with only start line number. +Recommendation: Check this line. + `; + + const results = parseMarkdownToDict(mdContent); + + expect(results).toHaveLength(1); + expect(results[0].extension.sourceLocation).toMatchObject({ + File: 'app.js', + startLine: 42 + }); + }); + + it('should handle missing sink location', () => { + const mdContent = ` +Vulnerability: No Sink Info +Vulnerability Type: Privacy +Severity: Low +Source Location: logger.ts:15 +Data Type: User ID +Line Content: console.log(user.id); +Description: Vulnerability without sink location details. +Recommendation: Use proper logging. + `; + + const results = parseMarkdownToDict(mdContent); + + expect(results).toHaveLength(1); + expect(results[0]).toMatchObject({ + vulnerability: 'No Sink Info', + vulnerabilityType: 'Privacy', + severity: 'Low' + }); + expect(results[0].extension.dataType).toBe('User ID'); + expect( + results[0].extension.sinkLocation === undefined || + (results[0].extension.sinkLocation?.File === null && + results[0].extension.sinkLocation?.startLine === null && + results[0].extension.sinkLocation?.endLine === null) + ).toBe(true); + }); +}); diff --git a/mcp-server/src/parser.ts b/mcp-server/src/parser.ts new file mode 100644 index 0000000..8cd3acc --- /dev/null +++ b/mcp-server/src/parser.ts @@ -0,0 +1,115 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export interface Location { + File: string | null; + startLine: number | null; + endLine: number | null; +} + +export interface Finding { + vulnerability: string | null; + vulnerabilityType: string | null; + severity: string | null; + extension: { + sourceLocation: Location; + sinkLocation: Location; + dataType: string | null; + }; + lineContent: string | null; + description: string | null; + recommendation: string | null; +} + +/** + * Parses a markdown string containing security findings into a structured format. + * The markdown should follow a specific format where each finding starts with "Vulnerability:" and includes fields like "Severity:", "Source Location:", etc. + * The function uses regular expressions to extract the relevant information and returns an array of findings. + * + * @param content - The markdown string to parse. + * @returns An array of structured findings extracted from the markdown. + */ +function parseLocation(locationStr: string | null): Location { + if (!locationStr) { + return { File: null, startLine: null, endLine: null }; + } + + const cleanStr = locationStr.replace(/`/g, '').trim(); + // Regex: path/file.ext:start-end or path/file.ext:line + // Matches: file.ext:12-34 OR file.ext:12 OR file.ext + const match = cleanStr.match(/^([^:]+)(?::(\d+)(?:-(\d+))?)?$/); + + if (match) { + const filePath = match[1].trim(); + let start: number | null = null; + let end: number | null = null; + if (match[2] && match[3]) { + start = parseInt(match[2], 10); + end = parseInt(match[3], 10); + } else if (match[2]) { + start = parseInt(match[2], 10); + end = start; + } + return { File: filePath, startLine: start, endLine: end }; + } + + return { File: cleanStr, startLine: null, endLine: null }; +} + +/** + * Parses a markdown string containing security findings into a structured format. + * The markdown should follow a specific format where each finding starts with "Vulnerability:" and includes fields like "Severity:", "Source Location:", etc. + * The function uses regular expressions to extract the relevant information and returns an array of findings. + * + * @param content - The markdown string to parse. + * @returns An array of structured findings extracted from the markdown. + */ +export function parseMarkdownToDict(content: string): Finding[] { + const findings: Finding[] = []; + + // Remove markdown formatting (bullet points at line start, ** markers), preserve hyphens in text + const cleanContent = content.replace(/^\s*[\*\-]\s*/gm, '').replace(/\*\*/g, ''); + + // Split each finding by "Vulnerability:" preceded by newline + const sections = cleanContent.split(/\n(?=#{1,6} |\s*Vulnerability:)/); + + for (let section of sections) { + section = section.trim(); + if (!section || !section.includes("Vulnerability:")) continue; + + const extract = (label: string): string | null => { + const fieldNames = 'Vulnerability|Severity|Source|Sink|Data|Line|Description|Recommendation'; + const patternStr = `(?:-?\\s*\\**)?${label}\\**:\\s*([\\s\\S]*?)(?=\\n(?:-?\\s*\\**)?(?:${fieldNames})|$)`; + const pattern = new RegExp(patternStr, 'i'); + const match = section.match(pattern); + return match ? match[1].trim() : null; + }; + + const rawSource = extract("Source Location"); + const rawSink = extract("Sink Location"); + + let lineContent = extract("Line Content"); + if (lineContent) { + lineContent = lineContent.replace(/^```[a-z]*\n|```$/gm, '').trim(); + } + + findings.push({ + vulnerability: extract("Vulnerability"), + vulnerabilityType: extract("Vulnerability Type"), + severity: extract("Severity"), + extension: { + sourceLocation: parseLocation(rawSource), + sinkLocation: parseLocation(rawSink), + dataType: extract("Data Type") + }, + lineContent, + description: extract("Description"), + recommendation: extract("Recommendation") + }); + } + + return findings; +} From 3faf18fe482317711597297b2c845d750177c935 Mon Sep 17 00:00:00 2001 From: Janet Vu Date: Fri, 6 Feb 2026 04:20:45 +0000 Subject: [PATCH 2/6] tighten instructions, set to specific file name only, and update checklist --- commands/security/analyze.toml | 12 +++--------- mcp-server/src/index.ts | 25 +++++++++++++------------ 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/commands/security/analyze.toml b/commands/security/analyze.toml index 68f9cf5..9b57729 100644 --- a/commands/security/analyze.toml +++ b/commands/security/analyze.toml @@ -20,13 +20,6 @@ Your primary objective during the **"SAST Recon on [file]"** task is to identify * `- [ ] 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. -## Skillset: Format Handling -* **Default Behavior**: Always provide the security analysis report in Markdown format within the conversation and as a file. -* **Conditional JSON Output**: If the user explicitly requests JSON (e.g., via a flag like `--json` or natural language), you MUST: - 1. Complete the standard Markdown report first. - 2. Call the `convert_report_to_json` tool using the path to the final report. - 3. Inform the user where the JSON file has been saved. - --- #### Role in the **Investigation Pass** @@ -74,13 +67,13 @@ For EVERY task, you MUST follow this procedure. This loop separates high-level s * **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:** If the user requested JSON output (e.g., via `--json` in context), call the `convert_report_to_json` tool now, using the current `DRAFT_SECURITY_REPORT.md` as the source. * **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:** ONLY AFTER the final report is delivered and any requested JSON conversion is complete, 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:** ONLY IF the user requested JSON output (e.g., via `--json` in context or natural language), call the `convert_report_to_json` tool. Inform the user that the JSON version of the report is available at .gemini_security/security_report.json. + * **Action:** After the final report is delivered and any requested JSON report is complete, 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. ### Example of the Workflow in `SECURITY_ANALYSIS_TODO.md` @@ -124,6 +117,7 @@ You will now begin executing the plan. The following are your precise instructio * 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. + * Addtionally, if the user requested JSON output (e.g., via `--json` in context or natural language), add a final task: - [ ] Generate JSON report. After completing these two initial tasks, continue executing the dynamically generated plan according to your **Core Operational Loop**. diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts index 8618467..525c19b 100644 --- a/mcp-server/src/index.ts +++ b/mcp-server/src/index.ts @@ -67,26 +67,27 @@ server.tool( server.tool( 'convert_report_to_json', - 'Converts a Markdown security report into a structured JSON format.', - { - reportPath: z.string().describe('Absolute path to the Markdown report file.'), - outputPath: z.string().optional().describe('Optional path to save the JSON output.'), - } as any, - (async (input: { reportPath: string; outputPath?: string }) => { + 'Converts the Markdown security report into a JSON file named security_report.json in the .gemini_security folder.', + {} as any, + (async () => { try { - const content = await fs.readFile(input.reportPath, 'utf-8'); + const reportPath = '.gemini_security/DRAFT_SECURITY_REPORT.md'; + const outputPath = '.gemini_security/security_report.json'; + + const content = await fs.readFile(reportPath, 'utf-8'); const results = parseMarkdownToDict(content); - if (input.outputPath) { - await fs.writeFile(input.outputPath, JSON.stringify(results, null, 2)); - } + await fs.writeFile(outputPath, JSON.stringify(results, null, 2)); return { - content: [{ type: 'text', text: JSON.stringify(results) }] + content: [{ + type: 'text', + text: `Successfully created JSON report at ${outputPath}` + }] }; } catch (error: any) { return { - content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }], + content: [{ type: 'text', text: `Error converting to JSON: ${error.message}` }], isError: true }; } From 3493140e697f7b58a2a858b8b7956f29746be00d Mon Sep 17 00:00:00 2001 From: jajanet Date: Thu, 5 Feb 2026 20:27:07 -0800 Subject: [PATCH 3/6] fix regex Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- mcp-server/src/parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcp-server/src/parser.ts b/mcp-server/src/parser.ts index 8cd3acc..9035915 100644 --- a/mcp-server/src/parser.ts +++ b/mcp-server/src/parser.ts @@ -81,7 +81,7 @@ export function parseMarkdownToDict(content: string): Finding[] { if (!section || !section.includes("Vulnerability:")) continue; const extract = (label: string): string | null => { - const fieldNames = 'Vulnerability|Severity|Source|Sink|Data|Line|Description|Recommendation'; + const fieldNames = 'Vulnerability Type|Severity|Source Location|Sink Location|Data Type|Line Content|Description|Recommendation'; const patternStr = `(?:-?\\s*\\**)?${label}\\**:\\s*([\\s\\S]*?)(?=\\n(?:-?\\s*\\**)?(?:${fieldNames})|$)`; const pattern = new RegExp(patternStr, 'i'); const match = section.match(pattern); From 1e3a81ffd25e722bd485f494b27dd6fd08d97270 Mon Sep 17 00:00:00 2001 From: jajanet Date: Thu, 5 Feb 2026 20:28:03 -0800 Subject: [PATCH 4/6] fix more regex Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- mcp-server/src/parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcp-server/src/parser.ts b/mcp-server/src/parser.ts index 9035915..5ef3600 100644 --- a/mcp-server/src/parser.ts +++ b/mcp-server/src/parser.ts @@ -89,7 +89,7 @@ export function parseMarkdownToDict(content: string): Finding[] { }; const rawSource = extract("Source Location"); - const rawSink = extract("Sink Location"); + const cleanContent = content.replace(/^\\s*[-*]\\s*/gm, '').replace(/\\*\\*/g, ''); let lineContent = extract("Line Content"); if (lineContent) { From 009798b9bfb736699358856f235f33f85a394a07 Mon Sep 17 00:00:00 2001 From: Janet Vu Date: Fri, 6 Feb 2026 04:34:37 +0000 Subject: [PATCH 5/6] add gemini suggestions --- commands/security/analyze.toml | 2 +- mcp-server/src/index.ts | 5 +++-- mcp-server/src/parser.ts | 10 +++++----- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/commands/security/analyze.toml b/commands/security/analyze.toml index 9b57729..c9dec2c 100644 --- a/commands/security/analyze.toml +++ b/commands/security/analyze.toml @@ -117,7 +117,7 @@ You will now begin executing the plan. The following are your precise instructio * 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. - * Addtionally, if the user requested JSON output (e.g., via `--json` in context or natural language), add a final task: - [ ] Generate JSON report. + * Additionally, if the user requested JSON output (e.g., via `--json` in context or natural language), add a final task: - [ ] Generate JSON report. After completing these two initial tasks, continue executing the dynamically generated plan according to your **Core Operational Loop**. diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts index 525c19b..a72e826 100644 --- a/mcp-server/src/index.ts +++ b/mcp-server/src/index.ts @@ -85,9 +85,10 @@ server.tool( text: `Successfully created JSON report at ${outputPath}` }] }; - } catch (error: any) { + } catch (error) { + const message = error instanceof Error ? error.message : String(error); return { - content: [{ type: 'text', text: `Error converting to JSON: ${error.message}` }], + content: [{ type: 'text', text: `Error converting to JSON: ${message}` }], isError: true }; } diff --git a/mcp-server/src/parser.ts b/mcp-server/src/parser.ts index 5ef3600..a45a390 100644 --- a/mcp-server/src/parser.ts +++ b/mcp-server/src/parser.ts @@ -5,7 +5,7 @@ */ export interface Location { - File: string | null; + file: string | null; startLine: number | null; endLine: number | null; } @@ -34,7 +34,7 @@ export interface Finding { */ function parseLocation(locationStr: string | null): Location { if (!locationStr) { - return { File: null, startLine: null, endLine: null }; + return { file: null, startLine: null, endLine: null }; } const cleanStr = locationStr.replace(/`/g, '').trim(); @@ -53,10 +53,10 @@ function parseLocation(locationStr: string | null): Location { start = parseInt(match[2], 10); end = start; } - return { File: filePath, startLine: start, endLine: end }; + return { file: filePath, startLine: start, endLine: end }; } - return { File: cleanStr, startLine: null, endLine: null }; + return { file: cleanStr, startLine: null, endLine: null }; } /** @@ -89,7 +89,7 @@ export function parseMarkdownToDict(content: string): Finding[] { }; const rawSource = extract("Source Location"); - const cleanContent = content.replace(/^\\s*[-*]\\s*/gm, '').replace(/\\*\\*/g, ''); + const rawSink = extract("Sink Location"); let lineContent = extract("Line Content"); if (lineContent) { From 9e2a85c6873b8d856271dd09a3d3e3cebaf847dc Mon Sep 17 00:00:00 2001 From: Janet Vu Date: Fri, 6 Feb 2026 04:57:46 +0000 Subject: [PATCH 6/6] fix tests --- mcp-server/src/parser.test.ts | 12 ++++++------ mcp-server/src/parser.ts | 10 ++++++---- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/mcp-server/src/parser.test.ts b/mcp-server/src/parser.test.ts index e44c794..79e7cb9 100644 --- a/mcp-server/src/parser.test.ts +++ b/mcp-server/src/parser.test.ts @@ -29,7 +29,7 @@ Recommendation: Move the secret to an environment variable. lineContent: 'const KEY = "sk_live_12345";', extension: { sourceLocation: { - File: 'config/settings.js', + file: 'config/settings.js', startLine: 15, endLine: 15 } @@ -55,7 +55,7 @@ Recommendation: Redact the email address before logging. expect(results).toHaveLength(1); expect(results[0].extension).toMatchObject({ sinkLocation: { - File: 'console.log', + file: 'console.log', startLine: 45, endLine: 45 }, @@ -102,7 +102,7 @@ Recommendation: Use a templating engine with auto-escaping. expect(results[0].vulnerability).toBe('Hardcoded Secret'); expect(results[0].severity).toBe('High'); - expect(results[0].extension.sourceLocation.File).toBe('index.js'); + expect(results[0].extension.sourceLocation.file).toBe('index.js'); expect(results[0].lineContent).toBe('const secret = "password";'); }); @@ -132,7 +132,7 @@ Recommendation: Verify the vulnerability details. severity: 'High', lineContent: 'const apiKey = process.env.API_KEY;' }); - expect(results[0].extension.sourceLocation.File).toBe('src/index.ts'); + expect(results[0].extension.sourceLocation.file).toBe('src/index.ts'); }); it('should handle missing end line number', () => { @@ -150,7 +150,7 @@ Recommendation: Check this line. expect(results).toHaveLength(1); expect(results[0].extension.sourceLocation).toMatchObject({ - File: 'app.js', + file: 'app.js', startLine: 42 }); }); @@ -178,7 +178,7 @@ Recommendation: Use proper logging. expect(results[0].extension.dataType).toBe('User ID'); expect( results[0].extension.sinkLocation === undefined || - (results[0].extension.sinkLocation?.File === null && + (results[0].extension.sinkLocation?.file === null && results[0].extension.sinkLocation?.startLine === null && results[0].extension.sinkLocation?.endLine === null) ).toBe(true); diff --git a/mcp-server/src/parser.ts b/mcp-server/src/parser.ts index a45a390..83fc75e 100644 --- a/mcp-server/src/parser.ts +++ b/mcp-server/src/parser.ts @@ -70,10 +70,12 @@ function parseLocation(locationStr: string | null): Location { export function parseMarkdownToDict(content: string): Finding[] { const findings: Finding[] = []; - // Remove markdown formatting (bullet points at line start, ** markers), preserve hyphens in text - const cleanContent = content.replace(/^\s*[\*\-]\s*/gm, '').replace(/\*\*/g, ''); + // Remove markdown bullet points (only at line start), markdown emphasis, and preserve hyphens/underscores in text + const cleanContent = content + .replace(/^\s*[\*\-]\s*/gm, '') // Remove bullet points at line start + .replace(/\*\*/g, ''); // Remove ** markdown - // Split each finding by "Vulnerability:" preceded by newline + // Split by "Vulnerability:" preceded by newline const sections = cleanContent.split(/\n(?=#{1,6} |\s*Vulnerability:)/); for (let section of sections) { @@ -81,7 +83,7 @@ export function parseMarkdownToDict(content: string): Finding[] { if (!section || !section.includes("Vulnerability:")) continue; const extract = (label: string): string | null => { - const fieldNames = 'Vulnerability Type|Severity|Source Location|Sink Location|Data Type|Line Content|Description|Recommendation'; + const fieldNames = 'Vulnerability|Severity|Source|Sink|Data|Line|Description|Recommendation'; const patternStr = `(?:-?\\s*\\**)?${label}\\**:\\s*([\\s\\S]*?)(?=\\n(?:-?\\s*\\**)?(?:${fieldNames})|$)`; const pattern = new RegExp(patternStr, 'i'); const match = section.match(pattern);