#
tokens: 5494/50000 4/4 files
lines: off (toggle) GitHub
raw markdown copy
# 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:
--------------------------------------------------------------------------------

```
bin/*
mcp-osv 
*.db

```

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

```markdown
# MCP Security Analyst

[![Go](https://github.com/gleicon/mcp-osv/actions/workflows/go.yml/badge.svg)](https://github.com/gleicon/mcp-osv/actions/workflows/go.yml)

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.

## Features

- Vulnerability checking using OSV.dev database
- Basic security analysis of code files
- Integration with AI models for security insights
- MCP protocol support for seamless integration with various AI tools
- Optional static code analysis using Semgrep (if installed)

## Cursor/Cline and other co-pilots IDE supply-chain preventions

`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.


## Requirements

### Core Requirements
```bash
make deps
make install
```

### Optional: Semgrep Installation
For enhanced static code analysis, you can install Semgrep:

#### macOS
```bash
brew install semgrep
```

#### Linux
```bash
python3 -m pip install semgrep
```

#### Other platforms
Visit [Semgrep Installation Guide](https://semgrep.dev/docs/getting-started/) for detailed instructions.

The MCP server will work without Semgrep installed, but will skip the static analysis portion when analyzing directories.

## Installation

```bash
make deps
make install
```

The mcp-osv command will be installed on PATH and use the stdin/stdout method.

Configure your LLM to use mcp-osv as an agent. 

For ***Cursor*** use the configuration below on `configuration` -> `MCP` tab:

```json
{"mcpServers":{"security_analyst":{"name":"Security Analyst","type":"stdio","command":"/usr/local/bin/mcp-osv"}}}
```

If you are using ***Claude*** just configure it under Settings -> Developer using the config below:

```json
{
    "mcpServers": {
        "mcp-osv": {
            "command": "/usr/local/bin/mcp-osv",
            "args": []
        }
    }
}
```

1. The server provides the following tools:

### check_vulnerabilities

Check for known vulnerabilities in dependencies using OSV.dev database.

Parameters:

- `package_name`: Name of the package to check
- `version`: Version of the package to check

### analyze_security

Analyze code for potential security issues based on https://osv.dev - a comprehensive database of open-source vulnerabilities. 

Parameters:

- `file_path`: Path to the file to analyze

## Integration with AI Models

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:

1. Check dependencies for known vulnerabilities
2. Analyze code for security issues
3. Provide recommendations for security improvements

## Connecting with Cursor

### Sample output
![output-1](screenshots/mcp-output-1.png)
![output-2](screenshots/mcp-output-2.png)
![output-3](screenshots/mcp-output-3.png)

### Usage

See mcp.json-template for an example that works with Cursor IDE.

After the setup, restart and ask something like "Analyze the security of my project using mcp-osv". 

To Debug in VSCode go to Help -> Toggle developer tools and at the console look for mcp.

To test the security analysis capabilities:
   

```bash
# Check for vulnerabilities in a package
"Check for vulnerabilities in the package 'express' version '4.17.1'"

# Analyze a specific file
"Analyze the security of the file 'main.go'"
```

The server will process your requests and provide security insights through the MCP protocol.


## Connect to Claude

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.)

```json
{
    "mcpServers": {
        "mcp_osv": {
            "command": "/usr/local/bin/mcp-osv",
            "args": []
        }
    }
}
````

## Development

To add new security analysis capabilities:

1. Create a new tool using `mcp.NewTool`
2. Implement the tool handler
3. Add the tool to the server using `s.AddTool`
4. check <https://github.com/mark3labs/mcp-go> for a comprehensive framework to build MCPs in Go.

## License

MIT 

```

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

```yaml
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go

name: Go

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:

  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4

    - name: Set up Go
      uses: actions/setup-go@v4
      with:
        go-version: '1.24'

    - name: Build
      run: go build -v ./...

    - name: Test
      run: go test -v ./...

```

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

```go
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
	"sync"
	"time"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
	"golang.org/x/time/rate"
)

// OSVResponse represents the response from OSV.dev API
type OSVResponse struct {
	Vulns []struct {
		ID      string `json:"id"`
		Summary string `json:"summary"`
		Details string `json:"details"`
		Affected []struct {
			Package struct {
				Name string `json:"name"`
			} `json:"package"`
		} `json:"affected"`
	} `json:"vulns"`
}

// RateLimiter implements a simple rate limiter for API calls
type RateLimiter struct {
	limiter *rate.Limiter
	mu      sync.Mutex
}

func NewRateLimiter(rps float64) *RateLimiter {
	return &RateLimiter{
		limiter: rate.NewLimiter(rate.Limit(rps), 1),
	}
}

func (r *RateLimiter) Wait(ctx context.Context) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	return r.limiter.Wait(ctx)
}

// Global rate limiter for OSV API calls (1 request per second)
var osvRateLimiter = NewRateLimiter(1)

// HTTP client with timeout
var httpClient = &http.Client{
	Timeout: 10 * time.Second,
	Transport: &http.Transport{
		MaxIdleConns:        10,
		IdleConnTimeout:     30 * time.Second,
		DisableCompression:  true,
		TLSHandshakeTimeout: 5 * time.Second,
	},
}

// Input validation functions
func isValidPackageName(name string) bool {
	return regexp.MustCompile(`^[a-zA-Z0-9\-\./]+$`).MatchString(name)
}

func isValidVersion(version string) bool {
	return regexp.MustCompile(`^[a-zA-Z0-9\-\./]+$`).MatchString(version)
}

func isSafePath(path string) (bool, string) {
	// Clean the path
	cleanPath := filepath.Clean(path)
	
	// Check for path traversal attempts
	if strings.Contains(cleanPath, "..") {
		return false, "path contains directory traversal patterns - .."
	}
	// Check for path traversal attempts
	if strings.HasPrefix(cleanPath, ".") {
		return false, "path contains directory traversal patterns - ."
	}
	// Check if the path exists
	_, err := os.Stat(cleanPath)
	if err != nil {
		if os.IsNotExist(err) {
			return false, fmt.Sprintf("path does not exist: %s", cleanPath)
		}
		return false, fmt.Sprintf("error accessing path: %v", err)
	}

	// Path exists and is accessible
	return true, ""
}

// SemgrepResult represents the JSON output from semgrep
type SemgrepResult struct {
	Results []struct {
		CheckID  string `json:"check_id"`
		Path     string `json:"path"`
		Start    struct {
			Line int `json:"line"`
		} `json:"start"`
		End struct {
			Line int `json:"line"`
		} `json:"end"`
		Extra struct {
			Message  string `json:"message"`
			Severity string `json:"severity"`
		} `json:"extra"`
	} `json:"results"`
}

var semgrepAvailable bool

func init() {
	// Check if semgrep is available
	_, err := exec.LookPath("semgrep")
	semgrepAvailable = err == nil
	if !semgrepAvailable {
		log.Println("Semgrep not found in PATH. Static code analysis will be limited.")
	}
}

func runSemgrep(path string) (string, error) {
	if !semgrepAvailable {
		return "\n=== Semgrep Analysis ===\n⚠️  Semgrep not installed. Static code analysis skipped.\nInstall Semgrep for enhanced security analysis.\n", nil
	}

	var result strings.Builder
	result.WriteString("\n=== Semgrep Analysis ===\n")

	cmd := exec.Command("semgrep", 
		"--config=auto",
		"--json",
		"--severity=WARNING",
		"--quiet",
		path)
	
	output, err := cmd.Output()
	if err != nil {
		if exitErr, ok := err.(*exec.ExitError); ok && len(exitErr.Stderr) > 0 {
			return "", fmt.Errorf("semgrep error: %s", exitErr.Stderr)
		}
		return "", fmt.Errorf("failed to run semgrep: %v", err)
	}

	var semgrepResult SemgrepResult
	if err := json.Unmarshal(output, &semgrepResult); err != nil {
		return "", fmt.Errorf("failed to parse semgrep output: %v", err)
	}

	// Add information about what was scanned
	result.WriteString("Running Semgrep security analysis...\n")
	
	if len(semgrepResult.Results) == 0 {
		result.WriteString("✅ No security issues found by Semgrep\n")
		return result.String(), nil
	}

	result.WriteString(fmt.Sprintf("Found %d potential security issues:\n\n", len(semgrepResult.Results)))
	for _, finding := range semgrepResult.Results {
		result.WriteString(fmt.Sprintf("⚠️  %s\n", finding.CheckID))
		result.WriteString(fmt.Sprintf("   Severity: %s\n", finding.Extra.Severity))
		result.WriteString(fmt.Sprintf("   File: %s (lines %d-%d)\n", finding.Path, finding.Start.Line, finding.End.Line))
		result.WriteString(fmt.Sprintf("   Message: %s\n\n", finding.Extra.Message))
	}

	return result.String(), nil
}

func main() {
	// Configure logging
	log.SetFlags(log.Ldate | log.Ltime | log.LUTC | log.Lshortfile)
	log.Println("Starting MCP Security Analyst server...")
	
	// Create MCP server
	s := server.NewMCPServer(
		"Security Analyst MCP",
		"1.0.0",
	)

	log.Println("Server created, adding tools...")

	// Add OSV vulnerability check tool
	osvTool := mcp.NewTool("check_vulnerabilities",
		mcp.WithDescription("Check for known vulnerabilities in dependencies"),
		mcp.WithString("package_name",
			mcp.Required(),
			mcp.Description("Name of the package to check"),
		),
		mcp.WithString("version",
			mcp.Required(),
			mcp.Description("Version of the package to check"),
		),
	)

	s.AddTool(osvTool, checkVulnerabilitiesHandler)
	log.Println("Added check_vulnerabilities tool")

	// Add security analysis tool
	analysisTool := mcp.NewTool("analyze_security",
		mcp.WithDescription("Analyze code for potential security issues in files or directories"),
		mcp.WithString("file_path",
			mcp.Required(),
			mcp.Description("Path to the file or directory to analyze"),
		),
	)

	s.AddTool(analysisTool, analyzeSecurityHandler)
	log.Println("Added analyze_security tool")

	log.Println("Starting stdio server...")
	// Start the stdio server
	if err := server.ServeStdio(s); err != nil {
		log.Printf("Server error: %v\n", err)
		os.Exit(1)
	}
}

func checkVulnerabilitiesHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	log.Printf("Received check_vulnerabilities request: %+v\n", request)
	
	// Input validation
	pkgName, ok := request.Params.Arguments["package_name"].(string)
	if !ok {
		return nil, fmt.Errorf("package_name must be a string")
	}
	if !isValidPackageName(pkgName) {
		return nil, fmt.Errorf("invalid package name format")
	}

	version, ok := request.Params.Arguments["version"].(string)
	if !ok {
		return nil, fmt.Errorf("version must be a string")
	}
	if !isValidVersion(version) {
		return nil, fmt.Errorf("invalid version format")
	}

	// Rate limiting
	if err := osvRateLimiter.Wait(ctx); err != nil {
		return nil, fmt.Errorf("rate limit exceeded: %v", err)
	}

	// Query OSV.dev API with sanitized inputs
	url := fmt.Sprintf("https://api.osv.dev/v1/query?package=%s&version=%s",
		strings.ReplaceAll(pkgName, " ", "+"),
		strings.ReplaceAll(version, " ", "+"))
	
	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %v", err)
	}

	resp, err := httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to query OSV API: %v", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read OSV response: %v", err)
	}

	var osvResp OSVResponse
	if err := json.Unmarshal(body, &osvResp); err != nil {
		return nil, fmt.Errorf("failed to parse OSV response: %v", err)
	}

	// Format response
	var result string
	if len(osvResp.Vulns) == 0 {
		result = fmt.Sprintf("No known vulnerabilities found for %s@%s", pkgName, version)
	} else {
		result = fmt.Sprintf("Found %d vulnerabilities for %s@%s:\n", len(osvResp.Vulns), pkgName, version)
		for _, vuln := range osvResp.Vulns {
			result += fmt.Sprintf("- %s: %s\n", vuln.ID, vuln.Summary)
			if vuln.Details != "" {
				result += fmt.Sprintf("  Details: %s\n", vuln.Details)
			}
		}
	}

	log.Printf("Completed vulnerability check for %s@%s\n", pkgName, version)
	return mcp.NewToolResultText(result), nil
}

func analyzeSecurityHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	log.Printf("Received analyze_security request: %+v\n", request)
	
	// Input validation
	path, ok := request.Params.Arguments["file_path"].(string)
	if !ok {
		return nil, fmt.Errorf("file_path must be a string")
	}
	
	// Clean and evaluate the path
	cleanPath := filepath.Clean(path)
	absPath, err := filepath.Abs(cleanPath)
	if err != nil {
		return nil, fmt.Errorf("failed to get absolute path: %v", err)
	}
	
	// Check if path exists and get info
	fileInfo, err := os.Stat(absPath)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, fmt.Errorf("path does not exist: %s", absPath)
		}
		return nil, fmt.Errorf("error accessing path: %v", err)
	}

	// Path safety check
	if strings.Contains(absPath, "..") {
		return nil, fmt.Errorf("unsafe path: contains directory traversal patterns")
	}

	var result strings.Builder
	if fileInfo.IsDir() {
		// Handle directory
		result.WriteString(fmt.Sprintf("Security analysis for directory: %s\n\n", absPath))
		
		// Run Semgrep analysis for the directory
		semgrepResult, semgrepErr := runSemgrep(absPath)
		if semgrepErr != nil {
			result.WriteString(fmt.Sprintf("Error running Semgrep: %v\n", semgrepErr))
		} else {
			result.WriteString(semgrepResult)
		}
		
		// Walk through the directory
		err := filepath.Walk(absPath, func(path string, info os.FileInfo, err error) error {
			if err != nil {
				result.WriteString(fmt.Sprintf("Error accessing %s: %v\n", path, err))
				return nil // Continue walking
			}

			// Skip directories themselves
			if info.IsDir() {
				return nil
			}

			// Skip non-analyzable files
			if !isAnalyzableFile(path) {
				return nil
			}

			// Analyze each file
			fileResult, err := analyzeFile(path)
			if err != nil {
				result.WriteString(fmt.Sprintf("Error analyzing %s: %v\n", path, err))
				return nil // Continue walking
			}

			result.WriteString(fmt.Sprintf("\n=== %s ===\n", path))
			result.WriteString(fileResult)
			result.WriteString("\n")

			return nil
		})

		if err != nil {
			return nil, fmt.Errorf("error walking directory: %v", err)
		}
	} else {
		// Handle single file
		fileResult, err := analyzeFile(absPath)
		if err != nil {
			return nil, fmt.Errorf("error analyzing file: %v", err)
		}
		result.WriteString(fileResult)
	}

	return mcp.NewToolResultText(result.String()), nil
}

func analyzeFile(filePath string) (string, error) {
	var result strings.Builder
	result.WriteString(fmt.Sprintf("File: %s\n", filePath))

	// Read the file
	content, err := os.ReadFile(filePath)
	if err != nil {
		return "", fmt.Errorf("failed to read file: %v", err)
	}

	contentStr := string(content)
	
	// Check for potential security issues
	checks := map[string]struct {
		pattern string
		desc    string
	}{
		"Hardcoded credentials": {
			pattern: `(?i)(password|secret|key|token|auth).*[=:]\s*['"][^'"]+['"]`,
			desc:    "Potential hardcoded credentials found",
		},
		"SQL injection risk": {
			pattern: `(?i)(db\.Query|db\.Exec|sql\.Open)\s*\(\s*([^,]+\+|fmt\.Sprintf).*?(SELECT|INSERT|UPDATE|DELETE)`,
			desc:    "Possible SQL injection risk - using string concatenation or formatting in database query",
		},
		"Insecure HTTP": {
			pattern: `http://[^/]*\.[^/]*`,
			desc:    "Insecure HTTP URL found (not HTTPS)",
		},
		"Command execution": {
			pattern: `exec\.(Command|CommandContext)`,
			desc:    "Command execution detected - validate inputs carefully",
		},
		"Hardcoded IPs": {
			pattern: `\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b`,
			desc:    "Hardcoded IP address found - consider configuration",
		},
	}

	foundIssues := false
	for checkName, check := range checks {
		if matched, err := regexp.MatchString(check.pattern, contentStr); err == nil && matched {
			result.WriteString(fmt.Sprintf("⚠️  %s\n   %s\n", checkName, check.desc))
			foundIssues = true
		}
	}

	// File-specific checks
	switch filepath.Base(filePath) {
	case "go.mod":
		// Check dependencies in go.mod
		deps, err := parseGoMod(contentStr)
		if err != nil {
			result.WriteString(fmt.Sprintf("Error parsing go.mod: %v\n", err))
		} else {
			result.WriteString("\nDependency analysis:\n")
			for _, dep := range deps {
				// Check each dependency with OSV
				vulns, err := checkOSVVulnerabilities(context.Background(), dep.name, dep.version)
				if err != nil {
					result.WriteString(fmt.Sprintf("Error checking %s@%s: %v\n", dep.name, dep.version, err))
					continue
				}
				if len(vulns) > 0 {
					result.WriteString(fmt.Sprintf("⚠️  %s@%s has %d known vulnerabilities:\n", dep.name, dep.version, len(vulns)))
					for _, vuln := range vulns {
						result.WriteString(fmt.Sprintf("   - %s: %s\n", vuln.ID, vuln.Summary))
					}
					foundIssues = true
				}
			}
		}
	}

	if !foundIssues {
		result.WriteString("✅ No immediate security concerns found\n")
	}

	return result.String(), nil
}

type dependency struct {
	name    string
	version string
}

func parseGoMod(content string) ([]dependency, error) {
	var deps []dependency
	lines := strings.Split(content, "\n")
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if strings.HasPrefix(line, "require ") || (len(line) > 0 && line[0] != ' ' && strings.Contains(line, " v")) {
			parts := strings.Fields(line)
			if len(parts) >= 2 {
				deps = append(deps, dependency{
					name:    parts[0],
					version: parts[1],
				})
			}
		}
	}
	return deps, nil
}

func checkOSVVulnerabilities(ctx context.Context, pkgName, version string) ([]struct{ID, Summary string}, error) {
	// Rate limiting
	if err := osvRateLimiter.Wait(ctx); err != nil {
		return nil, fmt.Errorf("rate limit exceeded: %v", err)
	}

	// Query OSV.dev API
	url := fmt.Sprintf("https://api.osv.dev/v1/query?package=%s&version=%s",
		strings.ReplaceAll(pkgName, " ", "+"),
		strings.ReplaceAll(version, " ", "+"))
	
	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %v", err)
	}

	resp, err := httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to query OSV API: %v", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read OSV response: %v", err)
	}

	var osvResp OSVResponse
	if err := json.Unmarshal(body, &osvResp); err != nil {
		return nil, fmt.Errorf("failed to parse OSV response: %v", err)
	}

	var vulns []struct{ID, Summary string}
	for _, v := range osvResp.Vulns {
		vulns = append(vulns, struct{ID, Summary string}{
			ID:      v.ID,
			Summary: v.Summary,
		})
	}
	return vulns, nil
}

func isAnalyzableFile(path string) bool {
	// List of file extensions to analyze
	analyzableExts := map[string]bool{
		".go":     true,
		".mod":    true,
		".sum":    true,
		".json":   true,
		".yaml":   true,
		".yml":    true,
		".toml":   true,
		".env":    true,
	}

	ext := strings.ToLower(filepath.Ext(path))
	base := filepath.Base(path)
	
	// Special files
	if base == "go.mod" || base == "go.sum" {
		return true
	}
	
	return analyzableExts[ext]
} 
```