Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
);

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