@indicated/vibeguard 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/.claude/settings.local.json +5 -0
  2. package/.github/workflows/ci.yml +65 -0
  3. package/.github/workflows/release.yml +85 -0
  4. package/PROGRESS.md +192 -0
  5. package/README.md +183 -0
  6. package/dist/api/license.d.ts +13 -0
  7. package/dist/api/license.d.ts.map +1 -0
  8. package/dist/api/license.js +138 -0
  9. package/dist/api/license.js.map +1 -0
  10. package/dist/api/rules.d.ts +13 -0
  11. package/dist/api/rules.d.ts.map +1 -0
  12. package/dist/api/rules.js +57 -0
  13. package/dist/api/rules.js.map +1 -0
  14. package/dist/cli/commands/init.d.ts +3 -0
  15. package/dist/cli/commands/init.d.ts.map +1 -0
  16. package/dist/cli/commands/init.js +145 -0
  17. package/dist/cli/commands/init.js.map +1 -0
  18. package/dist/cli/commands/login.d.ts +4 -0
  19. package/dist/cli/commands/login.d.ts.map +1 -0
  20. package/dist/cli/commands/login.js +121 -0
  21. package/dist/cli/commands/login.js.map +1 -0
  22. package/dist/cli/commands/mcp.d.ts +3 -0
  23. package/dist/cli/commands/mcp.d.ts.map +1 -0
  24. package/dist/cli/commands/mcp.js +14 -0
  25. package/dist/cli/commands/mcp.js.map +1 -0
  26. package/dist/cli/commands/rules.d.ts +3 -0
  27. package/dist/cli/commands/rules.d.ts.map +1 -0
  28. package/dist/cli/commands/rules.js +52 -0
  29. package/dist/cli/commands/rules.js.map +1 -0
  30. package/dist/cli/commands/scan.d.ts +3 -0
  31. package/dist/cli/commands/scan.d.ts.map +1 -0
  32. package/dist/cli/commands/scan.js +114 -0
  33. package/dist/cli/commands/scan.js.map +1 -0
  34. package/dist/cli/config.d.ts +4 -0
  35. package/dist/cli/config.d.ts.map +1 -0
  36. package/dist/cli/config.js +88 -0
  37. package/dist/cli/config.js.map +1 -0
  38. package/dist/cli/index.d.ts +3 -0
  39. package/dist/cli/index.d.ts.map +1 -0
  40. package/dist/cli/index.js +25 -0
  41. package/dist/cli/index.js.map +1 -0
  42. package/dist/cli/output.d.ts +15 -0
  43. package/dist/cli/output.d.ts.map +1 -0
  44. package/dist/cli/output.js +152 -0
  45. package/dist/cli/output.js.map +1 -0
  46. package/dist/mcp/server.d.ts +2 -0
  47. package/dist/mcp/server.d.ts.map +1 -0
  48. package/dist/mcp/server.js +188 -0
  49. package/dist/mcp/server.js.map +1 -0
  50. package/dist/scanner/index.d.ts +15 -0
  51. package/dist/scanner/index.d.ts.map +1 -0
  52. package/dist/scanner/index.js +207 -0
  53. package/dist/scanner/index.js.map +1 -0
  54. package/dist/scanner/parsers/javascript.d.ts +12 -0
  55. package/dist/scanner/parsers/javascript.d.ts.map +1 -0
  56. package/dist/scanner/parsers/javascript.js +266 -0
  57. package/dist/scanner/parsers/javascript.js.map +1 -0
  58. package/dist/scanner/parsers/python.d.ts +3 -0
  59. package/dist/scanner/parsers/python.d.ts.map +1 -0
  60. package/dist/scanner/parsers/python.js +108 -0
  61. package/dist/scanner/parsers/python.js.map +1 -0
  62. package/dist/scanner/rules/definitions.d.ts +5 -0
  63. package/dist/scanner/rules/definitions.d.ts.map +1 -0
  64. package/dist/scanner/rules/definitions.js +584 -0
  65. package/dist/scanner/rules/definitions.js.map +1 -0
  66. package/dist/scanner/rules/loader.d.ts +8 -0
  67. package/dist/scanner/rules/loader.d.ts.map +1 -0
  68. package/dist/scanner/rules/loader.js +45 -0
  69. package/dist/scanner/rules/loader.js.map +1 -0
  70. package/dist/scanner/rules/matcher.d.ts +11 -0
  71. package/dist/scanner/rules/matcher.d.ts.map +1 -0
  72. package/dist/scanner/rules/matcher.js +53 -0
  73. package/dist/scanner/rules/matcher.js.map +1 -0
  74. package/dist/types.d.ts +33 -0
  75. package/dist/types.d.ts.map +1 -0
  76. package/dist/types.js +3 -0
  77. package/dist/types.js.map +1 -0
  78. package/package.json +48 -0
  79. package/src/api/license.ts +120 -0
  80. package/src/api/rules.ts +70 -0
  81. package/src/cli/commands/init.ts +123 -0
  82. package/src/cli/commands/login.ts +92 -0
  83. package/src/cli/commands/mcp.ts +12 -0
  84. package/src/cli/commands/rules.ts +58 -0
  85. package/src/cli/commands/scan.ts +94 -0
  86. package/src/cli/config.ts +54 -0
  87. package/src/cli/index.ts +28 -0
  88. package/src/cli/output.ts +159 -0
  89. package/src/mcp/server.ts +195 -0
  90. package/src/scanner/index.ts +195 -0
  91. package/src/scanner/parsers/javascript.ts +285 -0
  92. package/src/scanner/parsers/python.ts +126 -0
  93. package/src/scanner/rules/definitions.ts +592 -0
  94. package/src/scanner/rules/loader.ts +59 -0
  95. package/src/scanner/rules/matcher.ts +68 -0
  96. package/src/types.ts +36 -0
  97. package/test-samples/secure.js +52 -0
  98. package/test-samples/vulnerable.js +56 -0
  99. package/test-samples/vulnerable.py +39 -0
  100. package/tests/helpers.ts +43 -0
  101. package/tests/rules/critical.test.ts +186 -0
  102. package/tests/rules/definitions.test.ts +167 -0
  103. package/tests/rules/high.test.ts +377 -0
  104. package/tests/rules/low.test.ts +172 -0
  105. package/tests/rules/medium.test.ts +224 -0
  106. package/tests/scanner/scanner.test.ts +161 -0
  107. package/tsconfig.json +19 -0
  108. package/vibe-coding-security-checklist.md +245 -0
  109. package/vitest.config.ts +15 -0
@@ -0,0 +1,94 @@
1
+ import { Command } from 'commander';
2
+ import * as path from 'path';
3
+ import { Scanner } from '../../scanner';
4
+ import { getLicenseKey } from '../../api/license';
5
+ import { loadConfig } from '../config';
6
+ import {
7
+ formatHeader,
8
+ formatScanning,
9
+ formatFinding,
10
+ formatSummary,
11
+ formatBlockedCommit,
12
+ formatCleanResult,
13
+ formatError,
14
+ shouldBlockCommit,
15
+ } from '../output';
16
+
17
+ const packageJson = require('../../../package.json');
18
+
19
+ export function createScanCommand(): Command {
20
+ const scan = new Command('scan')
21
+ .description('Scan files or directories for security vulnerabilities')
22
+ .argument('[targets...]', 'Files or directories to scan', ['.'])
23
+ .option('--staged', 'Scan only git staged files')
24
+ .option('--force', 'Continue even if critical/high issues found')
25
+ .option('--json', 'Output results as JSON')
26
+ .option('--quiet', 'Minimal output (exit code only)')
27
+ .action(async (targets: string[], options) => {
28
+ try {
29
+ const config = loadConfig();
30
+ const licenseKey = getLicenseKey();
31
+ const cwd = process.cwd();
32
+
33
+ const scanner = new Scanner(config);
34
+ await scanner.initialize(licenseKey || undefined);
35
+
36
+ if (!options.quiet && !options.json) {
37
+ console.log(formatHeader(packageJson.version));
38
+ }
39
+
40
+ // Perform scan
41
+ const result = options.staged
42
+ ? await scanner.scanStaged()
43
+ : await scanner.scan(targets.length > 0 ? targets : ['.']);
44
+
45
+ if (!options.quiet && !options.json) {
46
+ console.log(formatScanning(result.files));
47
+ }
48
+
49
+ // Output results
50
+ if (options.json) {
51
+ console.log(JSON.stringify({
52
+ version: packageJson.version,
53
+ files: result.files,
54
+ findings: result.findings.map(f => ({
55
+ rule: f.rule.id,
56
+ severity: f.rule.severity,
57
+ file: path.relative(cwd, f.file),
58
+ line: f.line,
59
+ column: f.column,
60
+ message: f.rule.name,
61
+ fix: f.rule.fix,
62
+ })),
63
+ duration: result.duration,
64
+ }, null, 2));
65
+ } else if (!options.quiet) {
66
+ if (result.findings.length === 0) {
67
+ console.log(formatCleanResult());
68
+ } else {
69
+ for (const finding of result.findings) {
70
+ console.log(formatFinding(finding, cwd));
71
+ }
72
+ console.log(formatSummary(result));
73
+ }
74
+ }
75
+
76
+ // Determine exit code
77
+ const hasBlockingIssues = shouldBlockCommit(result);
78
+
79
+ if (hasBlockingIssues && !options.force) {
80
+ if (!options.quiet && !options.json) {
81
+ console.log(formatBlockedCommit());
82
+ }
83
+ process.exit(1);
84
+ }
85
+
86
+ process.exit(0);
87
+ } catch (error) {
88
+ console.error(formatError(error instanceof Error ? error.message : 'Scan failed'));
89
+ process.exit(1);
90
+ }
91
+ });
92
+
93
+ return scan;
94
+ }
@@ -0,0 +1,54 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { Config } from '../types';
4
+
5
+ const CONFIG_FILES = [
6
+ '.vibeguardrc.json',
7
+ '.vibeguardrc',
8
+ 'vibeguard.config.json',
9
+ ];
10
+
11
+ export function loadConfig(cwd: string = process.cwd()): Config {
12
+ for (const configFile of CONFIG_FILES) {
13
+ const configPath = path.join(cwd, configFile);
14
+ if (fs.existsSync(configPath)) {
15
+ try {
16
+ const content = fs.readFileSync(configPath, 'utf-8');
17
+ return JSON.parse(content) as Config;
18
+ } catch {
19
+ // Invalid config, continue to next
20
+ }
21
+ }
22
+ }
23
+
24
+ // Check package.json for vibeguard key
25
+ const packageJsonPath = path.join(cwd, 'package.json');
26
+ if (fs.existsSync(packageJsonPath)) {
27
+ try {
28
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
29
+ if (pkg.vibeguard) {
30
+ return pkg.vibeguard as Config;
31
+ }
32
+ } catch {
33
+ // Invalid package.json
34
+ }
35
+ }
36
+
37
+ // Return default config
38
+ return {};
39
+ }
40
+
41
+ export function createDefaultConfig(): Config {
42
+ return {
43
+ exclude: [
44
+ 'node_modules',
45
+ 'dist',
46
+ 'build',
47
+ '.git',
48
+ 'coverage',
49
+ ],
50
+ rules: {
51
+ disabled: [],
52
+ },
53
+ };
54
+ }
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { createScanCommand } from './commands/scan';
5
+ import { createLoginCommand, createLogoutCommand } from './commands/login';
6
+ import { createInitCommand } from './commands/init';
7
+ import { createRulesCommand } from './commands/rules';
8
+ import { createMcpCommand } from './commands/mcp';
9
+
10
+ const packageJson = require('../../package.json');
11
+
12
+ const program = new Command();
13
+
14
+ program
15
+ .name('vibeguard')
16
+ .description('Local CLI security scanner for AI-generated code')
17
+ .version(packageJson.version);
18
+
19
+ // Add commands
20
+ program.addCommand(createScanCommand());
21
+ program.addCommand(createLoginCommand());
22
+ program.addCommand(createLogoutCommand());
23
+ program.addCommand(createInitCommand());
24
+ program.addCommand(createRulesCommand());
25
+ program.addCommand(createMcpCommand());
26
+
27
+ // Parse and execute
28
+ program.parse();
@@ -0,0 +1,159 @@
1
+ import { Finding, ScanResult, Severity, SecurityRule } from '../types';
2
+
3
+ // ANSI color codes (chalk is ESM-only, so we use direct codes for CommonJS compatibility)
4
+ const colors = {
5
+ reset: '\x1b[0m',
6
+ bold: '\x1b[1m',
7
+ dim: '\x1b[2m',
8
+ red: '\x1b[31m',
9
+ green: '\x1b[32m',
10
+ yellow: '\x1b[33m',
11
+ blue: '\x1b[34m',
12
+ magenta: '\x1b[35m',
13
+ cyan: '\x1b[36m',
14
+ white: '\x1b[37m',
15
+ bgRed: '\x1b[41m',
16
+ bgYellow: '\x1b[43m',
17
+ bgBlue: '\x1b[44m',
18
+ bgMagenta: '\x1b[45m',
19
+ };
20
+
21
+ const severityColors: Record<Severity, string> = {
22
+ critical: colors.bgRed + colors.white,
23
+ high: colors.red,
24
+ medium: colors.yellow,
25
+ low: colors.blue,
26
+ };
27
+
28
+ const severityLabels: Record<Severity, string> = {
29
+ critical: 'CRITICAL',
30
+ high: 'HIGH',
31
+ medium: 'MEDIUM',
32
+ low: 'LOW',
33
+ };
34
+
35
+ export function formatSeverity(severity: Severity): string {
36
+ const color = severityColors[severity];
37
+ const label = severityLabels[severity].padEnd(8);
38
+ return `${color}${colors.bold} ${label} ${colors.reset}`;
39
+ }
40
+
41
+ export function formatFinding(finding: Finding, cwd: string): string {
42
+ const relativePath = finding.file.replace(cwd + '/', '');
43
+ const location = `${relativePath}:${finding.line}`;
44
+ const severity = formatSeverity(finding.rule.severity);
45
+
46
+ let output = `\n${severity} ${colors.cyan}${location}${colors.reset}\n`;
47
+ output += ` ${finding.rule.name}\n`;
48
+
49
+ if (finding.rule.fix) {
50
+ output += ` ${colors.dim}→ ${finding.rule.fix}${colors.reset}\n`;
51
+ }
52
+
53
+ return output;
54
+ }
55
+
56
+ export function formatSummary(result: ScanResult): string {
57
+ const counts = {
58
+ critical: 0,
59
+ high: 0,
60
+ medium: 0,
61
+ low: 0,
62
+ };
63
+
64
+ for (const finding of result.findings) {
65
+ counts[finding.rule.severity]++;
66
+ }
67
+
68
+ const total = result.findings.length;
69
+ const grade = calculateGrade(counts);
70
+
71
+ let output = '\n';
72
+ output += `${colors.dim}─────────────────────────────────────────${colors.reset}\n`;
73
+ output += `Found ${colors.bold}${total}${colors.reset} issue${total !== 1 ? 's' : ''} `;
74
+ output += `(${colors.red}${counts.critical} critical${colors.reset}, `;
75
+ output += `${colors.yellow}${counts.high} high${colors.reset}, `;
76
+ output += `${colors.blue}${counts.medium} medium${colors.reset}, `;
77
+ output += `${colors.dim}${counts.low} low${colors.reset})\n\n`;
78
+
79
+ output += `Grade: ${formatGrade(grade)}\n`;
80
+
81
+ return output;
82
+ }
83
+
84
+ function calculateGrade(counts: Record<Severity, number>): string {
85
+ if (counts.critical > 0) return 'F';
86
+ if (counts.high > 2) return 'D';
87
+ if (counts.high > 0) return 'C';
88
+ if (counts.medium > 3) return 'C';
89
+ if (counts.medium > 0) return 'B';
90
+ if (counts.low > 5) return 'B';
91
+ if (counts.low > 0) return 'A';
92
+ return 'A+';
93
+ }
94
+
95
+ function formatGrade(grade: string): string {
96
+ const gradeColors: Record<string, string> = {
97
+ 'A+': colors.green + colors.bold,
98
+ 'A': colors.green,
99
+ 'B': colors.blue,
100
+ 'C': colors.yellow,
101
+ 'D': colors.red,
102
+ 'F': colors.bgRed + colors.white + colors.bold,
103
+ };
104
+
105
+ const color = gradeColors[grade] || colors.white;
106
+ return `${color}${grade}${colors.reset}`;
107
+ }
108
+
109
+ export function formatHeader(version: string): string {
110
+ return `\n${colors.cyan}${colors.bold}VibeGuard${colors.reset} Security Scanner ${colors.dim}v${version}${colors.reset}\n`;
111
+ }
112
+
113
+ export function formatScanning(fileCount: number): string {
114
+ return `\n${colors.dim}Scanning ${fileCount} file${fileCount !== 1 ? 's' : ''}...${colors.reset}\n`;
115
+ }
116
+
117
+ export function formatSuccess(message: string): string {
118
+ return `${colors.green}✓${colors.reset} ${message}`;
119
+ }
120
+
121
+ export function formatError(message: string): string {
122
+ return `${colors.red}✗${colors.reset} ${message}`;
123
+ }
124
+
125
+ export function formatWarning(message: string): string {
126
+ return `${colors.yellow}⚠${colors.reset} ${message}`;
127
+ }
128
+
129
+ export function formatInfo(message: string): string {
130
+ return `${colors.blue}ℹ${colors.reset} ${message}`;
131
+ }
132
+
133
+ export function formatRule(rule: SecurityRule): string {
134
+ const severity = formatSeverity(rule.severity);
135
+ let output = `${severity} ${colors.bold}${rule.id}${colors.reset}\n`;
136
+ output += ` ${rule.name}\n`;
137
+ output += ` ${colors.dim}${rule.description}${colors.reset}\n`;
138
+ if (rule.fix) {
139
+ output += ` ${colors.cyan}Fix: ${rule.fix}${colors.reset}\n`;
140
+ }
141
+ output += ` ${colors.dim}Languages: ${rule.languages.join(', ')}${colors.reset}\n`;
142
+ return output;
143
+ }
144
+
145
+ export function formatBlockedCommit(): string {
146
+ return `\n${colors.bgRed}${colors.white}${colors.bold} COMMIT BLOCKED ${colors.reset}\n` +
147
+ `${colors.red}Fix critical/high issues or use ${colors.bold}git commit --no-verify${colors.reset}${colors.red} to override.${colors.reset}\n`;
148
+ }
149
+
150
+ export function formatCleanResult(): string {
151
+ return `\n${colors.green}${colors.bold}✓ No security issues found!${colors.reset}\n` +
152
+ `\nGrade: ${formatGrade('A+')}\n`;
153
+ }
154
+
155
+ export function shouldBlockCommit(result: ScanResult): boolean {
156
+ return result.findings.some(
157
+ f => f.rule.severity === 'critical' || f.rule.severity === 'high'
158
+ );
159
+ }
@@ -0,0 +1,195 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { z } from 'zod';
4
+ import * as path from 'path';
5
+ import { Scanner } from '../scanner';
6
+ import { securityRules } from '../scanner/rules/definitions';
7
+
8
+ export async function startMcpServer(): Promise<void> {
9
+ const server = new McpServer({
10
+ name: 'vibeguard',
11
+ version: '1.0.0',
12
+ });
13
+
14
+ // Tool: scan_code
15
+ server.tool(
16
+ 'scan_code',
17
+ 'Scan files or directories for security vulnerabilities. Returns findings with severity, location, and fix suggestions. Use this after writing code or before commits.',
18
+ {
19
+ paths: z.array(z.string()).describe('File or directory paths to scan (relative to current working directory)'),
20
+ staged_only: z.boolean().optional().describe('If true, only scan git staged files'),
21
+ },
22
+ async ({ paths, staged_only }) => {
23
+ try {
24
+ const scanner = new Scanner();
25
+ await scanner.initialize();
26
+
27
+ const cwd = process.cwd();
28
+ const targets = paths.map(p => path.resolve(cwd, p));
29
+
30
+ const result = staged_only
31
+ ? await scanner.scanStaged()
32
+ : await scanner.scan(targets);
33
+
34
+ if (result.findings.length === 0) {
35
+ return {
36
+ content: [
37
+ {
38
+ type: 'text' as const,
39
+ text: `✅ No security issues found in ${result.files} file(s).`,
40
+ },
41
+ ],
42
+ };
43
+ }
44
+
45
+ // Format findings
46
+ const findings = result.findings.map(f => ({
47
+ severity: f.rule.severity,
48
+ rule: f.rule.id,
49
+ name: f.rule.name,
50
+ file: path.relative(cwd, f.file),
51
+ line: f.line,
52
+ message: f.rule.description,
53
+ fix: f.rule.fix,
54
+ }));
55
+
56
+ const counts = {
57
+ critical: findings.filter(f => f.severity === 'critical').length,
58
+ high: findings.filter(f => f.severity === 'high').length,
59
+ medium: findings.filter(f => f.severity === 'medium').length,
60
+ low: findings.filter(f => f.severity === 'low').length,
61
+ };
62
+
63
+ const summary = `Found ${findings.length} issue(s): ${counts.critical} critical, ${counts.high} high, ${counts.medium} medium, ${counts.low} low`;
64
+
65
+ const formattedFindings = findings.map(f =>
66
+ `[${f.severity.toUpperCase()}] ${f.file}:${f.line}\n ${f.name}\n Fix: ${f.fix}`
67
+ ).join('\n\n');
68
+
69
+ return {
70
+ content: [
71
+ {
72
+ type: 'text' as const,
73
+ text: `${summary}\n\n${formattedFindings}`,
74
+ },
75
+ ],
76
+ };
77
+ } catch (error) {
78
+ return {
79
+ content: [
80
+ {
81
+ type: 'text' as const,
82
+ text: `Error scanning: ${error instanceof Error ? error.message : 'Unknown error'}`,
83
+ },
84
+ ],
85
+ isError: true,
86
+ };
87
+ }
88
+ }
89
+ );
90
+
91
+ // Tool: list_security_rules
92
+ server.tool(
93
+ 'list_security_rules',
94
+ 'List all available security rules that VibeGuard checks for. Use this to understand what vulnerabilities are detected.',
95
+ {
96
+ severity: z.enum(['critical', 'high', 'medium', 'low']).optional().describe('Filter by severity level'),
97
+ },
98
+ async ({ severity }) => {
99
+ let rules = securityRules;
100
+
101
+ if (severity) {
102
+ rules = rules.filter(r => r.severity === severity);
103
+ }
104
+
105
+ const formatted = rules.map(r =>
106
+ `[${r.severity.toUpperCase()}] ${r.id}\n ${r.name}\n ${r.description}\n Languages: ${r.languages.join(', ')}`
107
+ ).join('\n\n');
108
+
109
+ return {
110
+ content: [
111
+ {
112
+ type: 'text' as const,
113
+ text: `${rules.length} security rule(s):\n\n${formatted}`,
114
+ },
115
+ ],
116
+ };
117
+ }
118
+ );
119
+
120
+ // Tool: check_code_snippet
121
+ server.tool(
122
+ 'check_code_snippet',
123
+ 'Check a code snippet for security vulnerabilities without writing to disk. Useful for validating code before suggesting it.',
124
+ {
125
+ code: z.string().describe('The code snippet to check'),
126
+ language: z.enum(['javascript', 'typescript', 'python']).describe('The programming language'),
127
+ },
128
+ async ({ code, language }) => {
129
+ try {
130
+ const fs = await import('fs');
131
+ const os = await import('os');
132
+
133
+ // Create temp file
134
+ const ext = language === 'python' ? '.py' : language === 'typescript' ? '.ts' : '.js';
135
+ const tempFile = path.join(os.tmpdir(), `vibeguard-check-${Date.now()}${ext}`);
136
+
137
+ fs.writeFileSync(tempFile, code);
138
+
139
+ const scanner = new Scanner();
140
+ await scanner.initialize();
141
+
142
+ const result = await scanner.scan([tempFile]);
143
+
144
+ // Clean up
145
+ fs.unlinkSync(tempFile);
146
+
147
+ if (result.findings.length === 0) {
148
+ return {
149
+ content: [
150
+ {
151
+ type: 'text' as const,
152
+ text: '✅ No security issues found in this code snippet.',
153
+ },
154
+ ],
155
+ };
156
+ }
157
+
158
+ const findings = result.findings.map(f => ({
159
+ severity: f.rule.severity,
160
+ rule: f.rule.id,
161
+ name: f.rule.name,
162
+ line: f.line,
163
+ fix: f.rule.fix,
164
+ }));
165
+
166
+ const formatted = findings.map(f =>
167
+ `[${f.severity.toUpperCase()}] Line ${f.line}: ${f.name}\n Fix: ${f.fix}`
168
+ ).join('\n\n');
169
+
170
+ return {
171
+ content: [
172
+ {
173
+ type: 'text' as const,
174
+ text: `Found ${findings.length} issue(s):\n\n${formatted}`,
175
+ },
176
+ ],
177
+ };
178
+ } catch (error) {
179
+ return {
180
+ content: [
181
+ {
182
+ type: 'text' as const,
183
+ text: `Error checking code: ${error instanceof Error ? error.message : 'Unknown error'}`,
184
+ },
185
+ ],
186
+ isError: true,
187
+ };
188
+ }
189
+ }
190
+ );
191
+
192
+ // Connect via stdio
193
+ const transport = new StdioServerTransport();
194
+ await server.connect(transport);
195
+ }