#
tokens: 7546/50000 4/4 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .github
│   └── workflows
│       └── go.yml
├── .gitignore
├── go.mod
├── go.sum
├── main.go
├── Makefile
├── mcp.json-template
├── README.md
├── screenshots
│   ├── mcp-output-1.png
│   ├── mcp-output-2.png
│   └── mcp-output-3.png
└── supply-chain-security-check.mdc
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | bin/*
2 | mcp-osv 
3 | *.db
4 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # MCP Security Analyst
  2 | 
  3 | [![Go](https://github.com/gleicon/mcp-osv/actions/workflows/go.yml/badge.svg)](https://github.com/gleicon/mcp-osv/actions/workflows/go.yml)
  4 | 
  5 | A Model Context Protocol (MCP) server that provides security analysis capabilities by integrating with OSV.dev and AI models to help identify and analyze potential vulnerabilities in your codebase.
  6 | 
  7 | ## Features
  8 | 
  9 | - Vulnerability checking using OSV.dev database
 10 | - Basic security analysis of code files
 11 | - Integration with AI models for security insights
 12 | - MCP protocol support for seamless integration with various AI tools
 13 | - Optional static code analysis using Semgrep (if installed)
 14 | 
 15 | ## Cursor/Cline and other co-pilots IDE supply-chain preventions
 16 | 
 17 | `mcp-osv` is the ideal companion for co-pilot coding. Use the [supply-chain-security-check.mdc] ruleset in this repo or build your own to manage dependencies and reduce risk. See below how to setup your IDE.
 18 | 
 19 | 
 20 | ## Requirements
 21 | 
 22 | ### Core Requirements
 23 | ```bash
 24 | make deps
 25 | make install
 26 | ```
 27 | 
 28 | ### Optional: Semgrep Installation
 29 | For enhanced static code analysis, you can install Semgrep:
 30 | 
 31 | #### macOS
 32 | ```bash
 33 | brew install semgrep
 34 | ```
 35 | 
 36 | #### Linux
 37 | ```bash
 38 | python3 -m pip install semgrep
 39 | ```
 40 | 
 41 | #### Other platforms
 42 | Visit [Semgrep Installation Guide](https://semgrep.dev/docs/getting-started/) for detailed instructions.
 43 | 
 44 | The MCP server will work without Semgrep installed, but will skip the static analysis portion when analyzing directories.
 45 | 
 46 | ## Installation
 47 | 
 48 | ```bash
 49 | make deps
 50 | make install
 51 | ```
 52 | 
 53 | The mcp-osv command will be installed on PATH and use the stdin/stdout method.
 54 | 
 55 | Configure your LLM to use mcp-osv as an agent. 
 56 | 
 57 | For ***Cursor*** use the configuration below on `configuration` -> `MCP` tab:
 58 | 
 59 | ```json
 60 | {"mcpServers":{"security_analyst":{"name":"Security Analyst","type":"stdio","command":"/usr/local/bin/mcp-osv"}}}
 61 | ```
 62 | 
 63 | If you are using ***Claude*** just configure it under Settings -> Developer using the config below:
 64 | 
 65 | ```json
 66 | {
 67 |     "mcpServers": {
 68 |         "mcp-osv": {
 69 |             "command": "/usr/local/bin/mcp-osv",
 70 |             "args": []
 71 |         }
 72 |     }
 73 | }
 74 | ```
 75 | 
 76 | 1. The server provides the following tools:
 77 | 
 78 | ### check_vulnerabilities
 79 | 
 80 | Check for known vulnerabilities in dependencies using OSV.dev database.
 81 | 
 82 | Parameters:
 83 | 
 84 | - `package_name`: Name of the package to check
 85 | - `version`: Version of the package to check
 86 | 
 87 | ### analyze_security
 88 | 
 89 | Analyze code for potential security issues based on https://osv.dev - a comprehensive database of open-source vulnerabilities. 
 90 | 
 91 | Parameters:
 92 | 
 93 | - `file_path`: Path to the file to analyze
 94 | 
 95 | ## Integration with AI Models
 96 | 
 97 | This server is designed to work with AI models like Claude and Cursor through the MCP protocol. The AI models can use the provided tools to:
 98 | 
 99 | 1. Check dependencies for known vulnerabilities
100 | 2. Analyze code for security issues
101 | 3. Provide recommendations for security improvements
102 | 
103 | ## Connecting with Cursor
104 | 
105 | ### Sample output
106 | ![output-1](screenshots/mcp-output-1.png)
107 | ![output-2](screenshots/mcp-output-2.png)
108 | ![output-3](screenshots/mcp-output-3.png)
109 | 
110 | ### Usage
111 | 
112 | See mcp.json-template for an example that works with Cursor IDE.
113 | 
114 | After the setup, restart and ask something like "Analyze the security of my project using mcp-osv". 
115 | 
116 | To Debug in VSCode go to Help -> Toggle developer tools and at the console look for mcp.
117 | 
118 | To test the security analysis capabilities:
119 |    
120 | 
121 | ```bash
122 | # Check for vulnerabilities in a package
123 | "Check for vulnerabilities in the package 'express' version '4.17.1'"
124 | 
125 | # Analyze a specific file
126 | "Analyze the security of the file 'main.go'"
127 | ```
128 | 
129 | The server will process your requests and provide security insights through the MCP protocol.
130 | 
131 | 
132 | ## Connect to Claude
133 | 
134 | Edit the config file and add the following section (that's the whole file, consider the mcp_osv section if you already have other tools installed.)
135 | 
136 | ```json
137 | {
138 |     "mcpServers": {
139 |         "mcp_osv": {
140 |             "command": "/usr/local/bin/mcp-osv",
141 |             "args": []
142 |         }
143 |     }
144 | }
145 | ````
146 | 
147 | ## Development
148 | 
149 | To add new security analysis capabilities:
150 | 
151 | 1. Create a new tool using `mcp.NewTool`
152 | 2. Implement the tool handler
153 | 3. Add the tool to the server using `s.AddTool`
154 | 4. check <https://github.com/mark3labs/mcp-go> for a comprehensive framework to build MCPs in Go.
155 | 
156 | ## License
157 | 
158 | MIT 
159 | 
```

--------------------------------------------------------------------------------
/.github/workflows/go.yml:
--------------------------------------------------------------------------------

```yaml
 1 | # This workflow will build a golang project
 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
 3 | 
 4 | name: Go
 5 | 
 6 | on:
 7 |   push:
 8 |     branches: [ "main" ]
 9 |   pull_request:
10 |     branches: [ "main" ]
11 | 
12 | jobs:
13 | 
14 |   build:
15 |     runs-on: ubuntu-latest
16 |     steps:
17 |     - uses: actions/checkout@v4
18 | 
19 |     - name: Set up Go
20 |       uses: actions/setup-go@v4
21 |       with:
22 |         go-version: '1.24'
23 | 
24 |     - name: Build
25 |       run: go build -v ./...
26 | 
27 |     - name: Test
28 |       run: go test -v ./...
29 | 
```

--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------

```go
  1 | package main
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"encoding/json"
  6 | 	"fmt"
  7 | 	"io"
  8 | 	"log"
  9 | 	"net/http"
 10 | 	"os"
 11 | 	"os/exec"
 12 | 	"path/filepath"
 13 | 	"regexp"
 14 | 	"strings"
 15 | 	"sync"
 16 | 	"time"
 17 | 
 18 | 	"github.com/mark3labs/mcp-go/mcp"
 19 | 	"github.com/mark3labs/mcp-go/server"
 20 | 	"golang.org/x/time/rate"
 21 | )
 22 | 
 23 | // OSVResponse represents the response from OSV.dev API
 24 | type OSVResponse struct {
 25 | 	Vulns []struct {
 26 | 		ID      string `json:"id"`
 27 | 		Summary string `json:"summary"`
 28 | 		Details string `json:"details"`
 29 | 		Affected []struct {
 30 | 			Package struct {
 31 | 				Name string `json:"name"`
 32 | 			} `json:"package"`
 33 | 		} `json:"affected"`
 34 | 	} `json:"vulns"`
 35 | }
 36 | 
 37 | // RateLimiter implements a simple rate limiter for API calls
 38 | type RateLimiter struct {
 39 | 	limiter *rate.Limiter
 40 | 	mu      sync.Mutex
 41 | }
 42 | 
 43 | func NewRateLimiter(rps float64) *RateLimiter {
 44 | 	return &RateLimiter{
 45 | 		limiter: rate.NewLimiter(rate.Limit(rps), 1),
 46 | 	}
 47 | }
 48 | 
 49 | func (r *RateLimiter) Wait(ctx context.Context) error {
 50 | 	r.mu.Lock()
 51 | 	defer r.mu.Unlock()
 52 | 	return r.limiter.Wait(ctx)
 53 | }
 54 | 
 55 | // Global rate limiter for OSV API calls (1 request per second)
 56 | var osvRateLimiter = NewRateLimiter(1)
 57 | 
 58 | // HTTP client with timeout
 59 | var httpClient = &http.Client{
 60 | 	Timeout: 10 * time.Second,
 61 | 	Transport: &http.Transport{
 62 | 		MaxIdleConns:        10,
 63 | 		IdleConnTimeout:     30 * time.Second,
 64 | 		DisableCompression:  true,
 65 | 		TLSHandshakeTimeout: 5 * time.Second,
 66 | 	},
 67 | }
 68 | 
 69 | // Input validation functions
 70 | func isValidPackageName(name string) bool {
 71 | 	return regexp.MustCompile(`^[a-zA-Z0-9\-\./]+$`).MatchString(name)
 72 | }
 73 | 
 74 | func isValidVersion(version string) bool {
 75 | 	return regexp.MustCompile(`^[a-zA-Z0-9\-\./]+$`).MatchString(version)
 76 | }
 77 | 
 78 | func isSafePath(path string) (bool, string) {
 79 | 	// Clean the path
 80 | 	cleanPath := filepath.Clean(path)
 81 | 	
 82 | 	// Check for path traversal attempts
 83 | 	if strings.Contains(cleanPath, "..") {
 84 | 		return false, "path contains directory traversal patterns - .."
 85 | 	}
 86 | 	// Check for path traversal attempts
 87 | 	if strings.HasPrefix(cleanPath, ".") {
 88 | 		return false, "path contains directory traversal patterns - ."
 89 | 	}
 90 | 	// Check if the path exists
 91 | 	_, err := os.Stat(cleanPath)
 92 | 	if err != nil {
 93 | 		if os.IsNotExist(err) {
 94 | 			return false, fmt.Sprintf("path does not exist: %s", cleanPath)
 95 | 		}
 96 | 		return false, fmt.Sprintf("error accessing path: %v", err)
 97 | 	}
 98 | 
 99 | 	// Path exists and is accessible
100 | 	return true, ""
101 | }
102 | 
103 | // SemgrepResult represents the JSON output from semgrep
104 | type SemgrepResult struct {
105 | 	Results []struct {
106 | 		CheckID  string `json:"check_id"`
107 | 		Path     string `json:"path"`
108 | 		Start    struct {
109 | 			Line int `json:"line"`
110 | 		} `json:"start"`
111 | 		End struct {
112 | 			Line int `json:"line"`
113 | 		} `json:"end"`
114 | 		Extra struct {
115 | 			Message  string `json:"message"`
116 | 			Severity string `json:"severity"`
117 | 		} `json:"extra"`
118 | 	} `json:"results"`
119 | }
120 | 
121 | var semgrepAvailable bool
122 | 
123 | func init() {
124 | 	// Check if semgrep is available
125 | 	_, err := exec.LookPath("semgrep")
126 | 	semgrepAvailable = err == nil
127 | 	if !semgrepAvailable {
128 | 		log.Println("Semgrep not found in PATH. Static code analysis will be limited.")
129 | 	}
130 | }
131 | 
132 | func runSemgrep(path string) (string, error) {
133 | 	if !semgrepAvailable {
134 | 		return "\n=== Semgrep Analysis ===\n⚠️  Semgrep not installed. Static code analysis skipped.\nInstall Semgrep for enhanced security analysis.\n", nil
135 | 	}
136 | 
137 | 	var result strings.Builder
138 | 	result.WriteString("\n=== Semgrep Analysis ===\n")
139 | 
140 | 	cmd := exec.Command("semgrep", 
141 | 		"--config=auto",
142 | 		"--json",
143 | 		"--severity=WARNING",
144 | 		"--quiet",
145 | 		path)
146 | 	
147 | 	output, err := cmd.Output()
148 | 	if err != nil {
149 | 		if exitErr, ok := err.(*exec.ExitError); ok && len(exitErr.Stderr) > 0 {
150 | 			return "", fmt.Errorf("semgrep error: %s", exitErr.Stderr)
151 | 		}
152 | 		return "", fmt.Errorf("failed to run semgrep: %v", err)
153 | 	}
154 | 
155 | 	var semgrepResult SemgrepResult
156 | 	if err := json.Unmarshal(output, &semgrepResult); err != nil {
157 | 		return "", fmt.Errorf("failed to parse semgrep output: %v", err)
158 | 	}
159 | 
160 | 	// Add information about what was scanned
161 | 	result.WriteString("Running Semgrep security analysis...\n")
162 | 	
163 | 	if len(semgrepResult.Results) == 0 {
164 | 		result.WriteString("✅ No security issues found by Semgrep\n")
165 | 		return result.String(), nil
166 | 	}
167 | 
168 | 	result.WriteString(fmt.Sprintf("Found %d potential security issues:\n\n", len(semgrepResult.Results)))
169 | 	for _, finding := range semgrepResult.Results {
170 | 		result.WriteString(fmt.Sprintf("⚠️  %s\n", finding.CheckID))
171 | 		result.WriteString(fmt.Sprintf("   Severity: %s\n", finding.Extra.Severity))
172 | 		result.WriteString(fmt.Sprintf("   File: %s (lines %d-%d)\n", finding.Path, finding.Start.Line, finding.End.Line))
173 | 		result.WriteString(fmt.Sprintf("   Message: %s\n\n", finding.Extra.Message))
174 | 	}
175 | 
176 | 	return result.String(), nil
177 | }
178 | 
179 | func main() {
180 | 	// Configure logging
181 | 	log.SetFlags(log.Ldate | log.Ltime | log.LUTC | log.Lshortfile)
182 | 	log.Println("Starting MCP Security Analyst server...")
183 | 	
184 | 	// Create MCP server
185 | 	s := server.NewMCPServer(
186 | 		"Security Analyst MCP",
187 | 		"1.0.0",
188 | 	)
189 | 
190 | 	log.Println("Server created, adding tools...")
191 | 
192 | 	// Add OSV vulnerability check tool
193 | 	osvTool := mcp.NewTool("check_vulnerabilities",
194 | 		mcp.WithDescription("Check for known vulnerabilities in dependencies"),
195 | 		mcp.WithString("package_name",
196 | 			mcp.Required(),
197 | 			mcp.Description("Name of the package to check"),
198 | 		),
199 | 		mcp.WithString("version",
200 | 			mcp.Required(),
201 | 			mcp.Description("Version of the package to check"),
202 | 		),
203 | 	)
204 | 
205 | 	s.AddTool(osvTool, checkVulnerabilitiesHandler)
206 | 	log.Println("Added check_vulnerabilities tool")
207 | 
208 | 	// Add security analysis tool
209 | 	analysisTool := mcp.NewTool("analyze_security",
210 | 		mcp.WithDescription("Analyze code for potential security issues in files or directories"),
211 | 		mcp.WithString("file_path",
212 | 			mcp.Required(),
213 | 			mcp.Description("Path to the file or directory to analyze"),
214 | 		),
215 | 	)
216 | 
217 | 	s.AddTool(analysisTool, analyzeSecurityHandler)
218 | 	log.Println("Added analyze_security tool")
219 | 
220 | 	log.Println("Starting stdio server...")
221 | 	// Start the stdio server
222 | 	if err := server.ServeStdio(s); err != nil {
223 | 		log.Printf("Server error: %v\n", err)
224 | 		os.Exit(1)
225 | 	}
226 | }
227 | 
228 | func checkVulnerabilitiesHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
229 | 	log.Printf("Received check_vulnerabilities request: %+v\n", request)
230 | 	
231 | 	// Input validation
232 | 	pkgName, ok := request.Params.Arguments["package_name"].(string)
233 | 	if !ok {
234 | 		return nil, fmt.Errorf("package_name must be a string")
235 | 	}
236 | 	if !isValidPackageName(pkgName) {
237 | 		return nil, fmt.Errorf("invalid package name format")
238 | 	}
239 | 
240 | 	version, ok := request.Params.Arguments["version"].(string)
241 | 	if !ok {
242 | 		return nil, fmt.Errorf("version must be a string")
243 | 	}
244 | 	if !isValidVersion(version) {
245 | 		return nil, fmt.Errorf("invalid version format")
246 | 	}
247 | 
248 | 	// Rate limiting
249 | 	if err := osvRateLimiter.Wait(ctx); err != nil {
250 | 		return nil, fmt.Errorf("rate limit exceeded: %v", err)
251 | 	}
252 | 
253 | 	// Query OSV.dev API with sanitized inputs
254 | 	url := fmt.Sprintf("https://api.osv.dev/v1/query?package=%s&version=%s",
255 | 		strings.ReplaceAll(pkgName, " ", "+"),
256 | 		strings.ReplaceAll(version, " ", "+"))
257 | 	
258 | 	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
259 | 	if err != nil {
260 | 		return nil, fmt.Errorf("failed to create request: %v", err)
261 | 	}
262 | 
263 | 	resp, err := httpClient.Do(req)
264 | 	if err != nil {
265 | 		return nil, fmt.Errorf("failed to query OSV API: %v", err)
266 | 	}
267 | 	defer resp.Body.Close()
268 | 
269 | 	body, err := io.ReadAll(resp.Body)
270 | 	if err != nil {
271 | 		return nil, fmt.Errorf("failed to read OSV response: %v", err)
272 | 	}
273 | 
274 | 	var osvResp OSVResponse
275 | 	if err := json.Unmarshal(body, &osvResp); err != nil {
276 | 		return nil, fmt.Errorf("failed to parse OSV response: %v", err)
277 | 	}
278 | 
279 | 	// Format response
280 | 	var result string
281 | 	if len(osvResp.Vulns) == 0 {
282 | 		result = fmt.Sprintf("No known vulnerabilities found for %s@%s", pkgName, version)
283 | 	} else {
284 | 		result = fmt.Sprintf("Found %d vulnerabilities for %s@%s:\n", len(osvResp.Vulns), pkgName, version)
285 | 		for _, vuln := range osvResp.Vulns {
286 | 			result += fmt.Sprintf("- %s: %s\n", vuln.ID, vuln.Summary)
287 | 			if vuln.Details != "" {
288 | 				result += fmt.Sprintf("  Details: %s\n", vuln.Details)
289 | 			}
290 | 		}
291 | 	}
292 | 
293 | 	log.Printf("Completed vulnerability check for %s@%s\n", pkgName, version)
294 | 	return mcp.NewToolResultText(result), nil
295 | }
296 | 
297 | func analyzeSecurityHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
298 | 	log.Printf("Received analyze_security request: %+v\n", request)
299 | 	
300 | 	// Input validation
301 | 	path, ok := request.Params.Arguments["file_path"].(string)
302 | 	if !ok {
303 | 		return nil, fmt.Errorf("file_path must be a string")
304 | 	}
305 | 	
306 | 	// Clean and evaluate the path
307 | 	cleanPath := filepath.Clean(path)
308 | 	absPath, err := filepath.Abs(cleanPath)
309 | 	if err != nil {
310 | 		return nil, fmt.Errorf("failed to get absolute path: %v", err)
311 | 	}
312 | 	
313 | 	// Check if path exists and get info
314 | 	fileInfo, err := os.Stat(absPath)
315 | 	if err != nil {
316 | 		if os.IsNotExist(err) {
317 | 			return nil, fmt.Errorf("path does not exist: %s", absPath)
318 | 		}
319 | 		return nil, fmt.Errorf("error accessing path: %v", err)
320 | 	}
321 | 
322 | 	// Path safety check
323 | 	if strings.Contains(absPath, "..") {
324 | 		return nil, fmt.Errorf("unsafe path: contains directory traversal patterns")
325 | 	}
326 | 
327 | 	var result strings.Builder
328 | 	if fileInfo.IsDir() {
329 | 		// Handle directory
330 | 		result.WriteString(fmt.Sprintf("Security analysis for directory: %s\n\n", absPath))
331 | 		
332 | 		// Run Semgrep analysis for the directory
333 | 		semgrepResult, semgrepErr := runSemgrep(absPath)
334 | 		if semgrepErr != nil {
335 | 			result.WriteString(fmt.Sprintf("Error running Semgrep: %v\n", semgrepErr))
336 | 		} else {
337 | 			result.WriteString(semgrepResult)
338 | 		}
339 | 		
340 | 		// Walk through the directory
341 | 		err := filepath.Walk(absPath, func(path string, info os.FileInfo, err error) error {
342 | 			if err != nil {
343 | 				result.WriteString(fmt.Sprintf("Error accessing %s: %v\n", path, err))
344 | 				return nil // Continue walking
345 | 			}
346 | 
347 | 			// Skip directories themselves
348 | 			if info.IsDir() {
349 | 				return nil
350 | 			}
351 | 
352 | 			// Skip non-analyzable files
353 | 			if !isAnalyzableFile(path) {
354 | 				return nil
355 | 			}
356 | 
357 | 			// Analyze each file
358 | 			fileResult, err := analyzeFile(path)
359 | 			if err != nil {
360 | 				result.WriteString(fmt.Sprintf("Error analyzing %s: %v\n", path, err))
361 | 				return nil // Continue walking
362 | 			}
363 | 
364 | 			result.WriteString(fmt.Sprintf("\n=== %s ===\n", path))
365 | 			result.WriteString(fileResult)
366 | 			result.WriteString("\n")
367 | 
368 | 			return nil
369 | 		})
370 | 
371 | 		if err != nil {
372 | 			return nil, fmt.Errorf("error walking directory: %v", err)
373 | 		}
374 | 	} else {
375 | 		// Handle single file
376 | 		fileResult, err := analyzeFile(absPath)
377 | 		if err != nil {
378 | 			return nil, fmt.Errorf("error analyzing file: %v", err)
379 | 		}
380 | 		result.WriteString(fileResult)
381 | 	}
382 | 
383 | 	return mcp.NewToolResultText(result.String()), nil
384 | }
385 | 
386 | func analyzeFile(filePath string) (string, error) {
387 | 	var result strings.Builder
388 | 	result.WriteString(fmt.Sprintf("File: %s\n", filePath))
389 | 
390 | 	// Read the file
391 | 	content, err := os.ReadFile(filePath)
392 | 	if err != nil {
393 | 		return "", fmt.Errorf("failed to read file: %v", err)
394 | 	}
395 | 
396 | 	contentStr := string(content)
397 | 	
398 | 	// Check for potential security issues
399 | 	checks := map[string]struct {
400 | 		pattern string
401 | 		desc    string
402 | 	}{
403 | 		"Hardcoded credentials": {
404 | 			pattern: `(?i)(password|secret|key|token|auth).*[=:]\s*['"][^'"]+['"]`,
405 | 			desc:    "Potential hardcoded credentials found",
406 | 		},
407 | 		"SQL injection risk": {
408 | 			pattern: `(?i)(db\.Query|db\.Exec|sql\.Open)\s*\(\s*([^,]+\+|fmt\.Sprintf).*?(SELECT|INSERT|UPDATE|DELETE)`,
409 | 			desc:    "Possible SQL injection risk - using string concatenation or formatting in database query",
410 | 		},
411 | 		"Insecure HTTP": {
412 | 			pattern: `http://[^/]*\.[^/]*`,
413 | 			desc:    "Insecure HTTP URL found (not HTTPS)",
414 | 		},
415 | 		"Command execution": {
416 | 			pattern: `exec\.(Command|CommandContext)`,
417 | 			desc:    "Command execution detected - validate inputs carefully",
418 | 		},
419 | 		"Hardcoded IPs": {
420 | 			pattern: `\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b`,
421 | 			desc:    "Hardcoded IP address found - consider configuration",
422 | 		},
423 | 	}
424 | 
425 | 	foundIssues := false
426 | 	for checkName, check := range checks {
427 | 		if matched, err := regexp.MatchString(check.pattern, contentStr); err == nil && matched {
428 | 			result.WriteString(fmt.Sprintf("⚠️  %s\n   %s\n", checkName, check.desc))
429 | 			foundIssues = true
430 | 		}
431 | 	}
432 | 
433 | 	// File-specific checks
434 | 	switch filepath.Base(filePath) {
435 | 	case "go.mod":
436 | 		// Check dependencies in go.mod
437 | 		deps, err := parseGoMod(contentStr)
438 | 		if err != nil {
439 | 			result.WriteString(fmt.Sprintf("Error parsing go.mod: %v\n", err))
440 | 		} else {
441 | 			result.WriteString("\nDependency analysis:\n")
442 | 			for _, dep := range deps {
443 | 				// Check each dependency with OSV
444 | 				vulns, err := checkOSVVulnerabilities(context.Background(), dep.name, dep.version)
445 | 				if err != nil {
446 | 					result.WriteString(fmt.Sprintf("Error checking %s@%s: %v\n", dep.name, dep.version, err))
447 | 					continue
448 | 				}
449 | 				if len(vulns) > 0 {
450 | 					result.WriteString(fmt.Sprintf("⚠️  %s@%s has %d known vulnerabilities:\n", dep.name, dep.version, len(vulns)))
451 | 					for _, vuln := range vulns {
452 | 						result.WriteString(fmt.Sprintf("   - %s: %s\n", vuln.ID, vuln.Summary))
453 | 					}
454 | 					foundIssues = true
455 | 				}
456 | 			}
457 | 		}
458 | 	}
459 | 
460 | 	if !foundIssues {
461 | 		result.WriteString("✅ No immediate security concerns found\n")
462 | 	}
463 | 
464 | 	return result.String(), nil
465 | }
466 | 
467 | type dependency struct {
468 | 	name    string
469 | 	version string
470 | }
471 | 
472 | func parseGoMod(content string) ([]dependency, error) {
473 | 	var deps []dependency
474 | 	lines := strings.Split(content, "\n")
475 | 	for _, line := range lines {
476 | 		line = strings.TrimSpace(line)
477 | 		if strings.HasPrefix(line, "require ") || (len(line) > 0 && line[0] != ' ' && strings.Contains(line, " v")) {
478 | 			parts := strings.Fields(line)
479 | 			if len(parts) >= 2 {
480 | 				deps = append(deps, dependency{
481 | 					name:    parts[0],
482 | 					version: parts[1],
483 | 				})
484 | 			}
485 | 		}
486 | 	}
487 | 	return deps, nil
488 | }
489 | 
490 | func checkOSVVulnerabilities(ctx context.Context, pkgName, version string) ([]struct{ID, Summary string}, error) {
491 | 	// Rate limiting
492 | 	if err := osvRateLimiter.Wait(ctx); err != nil {
493 | 		return nil, fmt.Errorf("rate limit exceeded: %v", err)
494 | 	}
495 | 
496 | 	// Query OSV.dev API
497 | 	url := fmt.Sprintf("https://api.osv.dev/v1/query?package=%s&version=%s",
498 | 		strings.ReplaceAll(pkgName, " ", "+"),
499 | 		strings.ReplaceAll(version, " ", "+"))
500 | 	
501 | 	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
502 | 	if err != nil {
503 | 		return nil, fmt.Errorf("failed to create request: %v", err)
504 | 	}
505 | 
506 | 	resp, err := httpClient.Do(req)
507 | 	if err != nil {
508 | 		return nil, fmt.Errorf("failed to query OSV API: %v", err)
509 | 	}
510 | 	defer resp.Body.Close()
511 | 
512 | 	body, err := io.ReadAll(resp.Body)
513 | 	if err != nil {
514 | 		return nil, fmt.Errorf("failed to read OSV response: %v", err)
515 | 	}
516 | 
517 | 	var osvResp OSVResponse
518 | 	if err := json.Unmarshal(body, &osvResp); err != nil {
519 | 		return nil, fmt.Errorf("failed to parse OSV response: %v", err)
520 | 	}
521 | 
522 | 	var vulns []struct{ID, Summary string}
523 | 	for _, v := range osvResp.Vulns {
524 | 		vulns = append(vulns, struct{ID, Summary string}{
525 | 			ID:      v.ID,
526 | 			Summary: v.Summary,
527 | 		})
528 | 	}
529 | 	return vulns, nil
530 | }
531 | 
532 | func isAnalyzableFile(path string) bool {
533 | 	// List of file extensions to analyze
534 | 	analyzableExts := map[string]bool{
535 | 		".go":     true,
536 | 		".mod":    true,
537 | 		".sum":    true,
538 | 		".json":   true,
539 | 		".yaml":   true,
540 | 		".yml":    true,
541 | 		".toml":   true,
542 | 		".env":    true,
543 | 	}
544 | 
545 | 	ext := strings.ToLower(filepath.Ext(path))
546 | 	base := filepath.Base(path)
547 | 	
548 | 	// Special files
549 | 	if base == "go.mod" || base == "go.sum" {
550 | 		return true
551 | 	}
552 | 	
553 | 	return analyzableExts[ext]
554 | } 
```