Skip to content
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ By default, the `/security:analyze` command determines the scope of the analysis
/security:analyze Analyze all the source code under the script folder. Skip the docs, config files and package files.
```

To get the security report in JSON format, you can use the `--json` flag or request JSON output using natural language:
```bash
/security:analyze --json
```

Or alternatively:
```bash
/security:analyze Return the report in JSON format.
```

![Customize analysis command](./assets/customize_command.gif)

### Scan for vulnerable dependencies
Expand Down
4 changes: 3 additions & 1 deletion commands/security/analyze.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ 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:** 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 ONLY the temporary files (`SECURITY_ANALYSIS_TODO.md` and `DRAFT_SECURITY_REPORT.md`, you must keep `security_report.json` if generated) 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`
Expand Down Expand Up @@ -116,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.
* 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**.

Expand Down
31 changes: 31 additions & 0 deletions mcp-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -64,6 +65,36 @@ server.tool(
(input: { filePath: string }) => runPoc(input)
);

server.tool(
'convert_report_to_json',
'Converts the Markdown security report into a JSON file named security_report.json in the .gemini_security folder.',
{} as any,
(async () => {
try {
const reportPath = path.join(process.cwd(), '.gemini_security/DRAFT_SECURITY_REPORT.md');
const outputPath = path.join(process.cwd(), '.gemini_security/security_report.json');

const content = await fs.readFile(reportPath, 'utf-8');
const results = parseMarkdownToDict(content);

await fs.writeFile(outputPath, JSON.stringify(results, null, 2));

return {
content: [{
type: 'text',
text: `Successfully created JSON report at ${outputPath}`
}]
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
content: [{ type: 'text', text: `Error converting to JSON: ${message}` }],
isError: true
};
}
}) as any
);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Casting the function to `any` using `as any` bypasses TypeScript's type safety checks. It would be more robust to define and use a specific type for the server tool implementation to ensure type safety and prevent potential runtime errors.

server.registerPrompt(
'security:note-adder',
{
Expand Down
184 changes: 184 additions & 0 deletions mcp-server/src/parser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/**
* @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";',
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]).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].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].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].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].dataType).toBe('User ID');
expect(
results[0].sinkLocation === undefined ||
(results[0].sinkLocation?.file === null &&
results[0].sinkLocation?.startLine === null &&
results[0].sinkLocation?.endLine === null)
).toBe(true);
});
});
Loading
Loading